├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── bug_report.md ├── .gitignore ├── .nvmrc ├── .yarn ├── plugins │ └── @yarnpkg │ │ ├── plugin-interactive-tools.cjs │ │ └── plugin-workspace-tools.cjs └── releases │ └── yarn-3.6.4.cjs ├── .yarnrc.yml ├── LICENSE ├── README.md ├── babel.config.js ├── docs ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── README.KR.md ├── SECURITY.md └── imgs │ ├── rn-inner-shadow-gif.gif │ ├── rn-inner-shadow-pressable-thumbnail.gif │ └── rn-inner-shadow-thubnail.jpg ├── example ├── app.json ├── assets │ ├── adaptive-icon.png │ ├── favicon.png │ ├── icon.png │ └── splash-icon.png ├── babel.config.js ├── index.js ├── metro.config.js ├── package.json ├── src │ └── App.tsx └── tsconfig.json ├── lefthook.yml ├── package.json ├── src ├── components │ ├── CornerRadii.tsx │ ├── ShadowLinearGradientFill.tsx │ ├── ShadowPressable.tsx │ ├── ShadowToggle.tsx │ └── ShadowView.tsx ├── constants.ts ├── hooks │ ├── useAnimatedOffset.tsx │ └── useShadowProperties.tsx ├── index.ts ├── types.ts └── utils.ts ├── tsconfig.build.json └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | indent_style = space 10 | indent_size = 2 11 | 12 | end_of_line = lf 13 | charset = utf-8 14 | trim_trailing_whitespace = true 15 | insert_final_newline = true 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## I'm submitting a ... 2 | - [ ] Feature request 3 | - [ ] Bug report 4 | 5 | ## Description 6 | Briefly describe the motivation and changes for reviewer and provide a list of changes underneath 7 | 8 | ## Resources 9 | - Issue: - Issue Link (N/A) 10 | - Discussion : - Discussion Link (N/A) 11 | 12 | ## Type of change 13 | - [ ] Bug fix (non-breaking change which fixes an issue) 14 | - [ ] New feature (non-breaking change which adds functionality) 15 | - [ ] This change requires a documentation update 16 | - [ ] etc ... 17 | 18 | ## Note for reviewer 19 | Is there anything code reviewer has to know before code review? 20 | 21 | ## Did you test this on any platform below? 22 | - [ ] iOS 23 | - [ ] Android 24 | -------------------------------------------------------------------------------- /.github/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 16 | 1. Go to '...' 17 | 2. Click on '....' 18 | 3. Scroll down to '....' 19 | 4. See error 20 | 21 | **Expected behavior** 22 | A clear and concise description of what you expected to happen. 23 | 24 | **Screenshots** 25 | If applicable, add screenshots to help explain your problem. 26 | 27 | **Desktop (please complete the following information):** 28 | 29 | - OS: [e.g. iOS] 30 | - Browser [e.g. chrome, safari] 31 | - Version [e.g. 22] 32 | 33 | **Smartphone (please complete the following information):** 34 | 35 | - Device: [e.g. iPhone6] 36 | - OS: [e.g. iOS8.1] 37 | - Browser [e.g. stock browser, safari] 38 | - Version [e.g. 22] 39 | 40 | **Additional context** 41 | Add any other context about the problem here. 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | .DS_Store 3 | 4 | # VSCode 5 | .vscode/ 6 | jsconfig.json 7 | 8 | # Xcode 9 | build/ 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata 19 | *.xccheckout 20 | *.moved-aside 21 | DerivedData 22 | *.hmap 23 | *.ipa 24 | *.xcuserstate 25 | **/.xcode.env.local 26 | project.xcworkspace 27 | 28 | # Ruby / CocoaPods 29 | **/Pods/ 30 | /vendor/bundle/ 31 | 32 | .watchmanconfig 33 | Gemfile.lock 34 | Gemfile 35 | 36 | # Bundle artifact 37 | *.jsbundle 38 | .bundle 39 | 40 | # Android/IntelliJ 41 | build/ 42 | .idea 43 | .gradle 44 | local.properties 45 | *.iml 46 | *.hprof 47 | .cxx/ 48 | *.keystore 49 | !debug.keystore 50 | # Android/IJ 51 | .classpath 52 | .cxx 53 | .gradle 54 | .idea 55 | .project 56 | .settings 57 | local.properties 58 | android.iml 59 | # BUCK 60 | buck-out/ 61 | \.buckd/ 62 | android/app/libs 63 | android/keystores/debug.keystore 64 | 65 | # node.js 66 | node_modules/ 67 | npm-debug.log 68 | yarn-error.log 69 | yarn-debug.log 70 | 71 | # Yarn 72 | .yarn/* 73 | yarn.lock 74 | !.yarn/patches 75 | !.yarn/plugins 76 | !.yarn/releases 77 | !.yarn/sdks 78 | !.yarn/versions 79 | !.yarn/versions 80 | 81 | 82 | # formatting 83 | .prettierrc.js 84 | .eslintrc.js 85 | 86 | # Expo 87 | .expo/ 88 | 89 | # Turborepo 90 | .turbo/ 91 | 92 | # React Native Codegen 93 | ios/generated 94 | android/generated 95 | 96 | # generated by bob 97 | lib/ 98 | 99 | # fastlane 100 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 101 | # screenshots whenever they are needed. 102 | # For more information about the recommended setup visit: 103 | # https://docs.fastlane.tools/best-practices/source-control/ 104 | **/fastlane/report.xml 105 | **/fastlane/Preview.html 106 | **/fastlane/screenshots 107 | **/fastlane/test_output 108 | 109 | # Temporary files created by Metro to check the health of the file watcher 110 | .metro-health-check* 111 | 112 | # testing 113 | jest.config.js 114 | /coverage 115 | 116 | # Example 117 | example/node_modules/ 118 | # example > Ruby 119 | example/vendor/ 120 | # example > Cocoapods 121 | example/ios/Pods 122 | 123 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18 2 | -------------------------------------------------------------------------------- /.yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | //prettier-ignore 3 | module.exports = { 4 | name: "@yarnpkg/plugin-workspace-tools", 5 | factory: function (require) { 6 | var plugin=(()=>{var yr=Object.create;var we=Object.defineProperty;var _r=Object.getOwnPropertyDescriptor;var Er=Object.getOwnPropertyNames;var br=Object.getPrototypeOf,xr=Object.prototype.hasOwnProperty;var W=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(r,t)=>(typeof require<"u"?require:r)[t]}):e)(function(e){if(typeof require<"u")return require.apply(this,arguments);throw new Error('Dynamic require of "'+e+'" is not supported')});var q=(e,r)=>()=>(r||e((r={exports:{}}).exports,r),r.exports),Cr=(e,r)=>{for(var t in r)we(e,t,{get:r[t],enumerable:!0})},Je=(e,r,t,n)=>{if(r&&typeof r=="object"||typeof r=="function")for(let s of Er(r))!xr.call(e,s)&&s!==t&&we(e,s,{get:()=>r[s],enumerable:!(n=_r(r,s))||n.enumerable});return e};var Be=(e,r,t)=>(t=e!=null?yr(br(e)):{},Je(r||!e||!e.__esModule?we(t,"default",{value:e,enumerable:!0}):t,e)),wr=e=>Je(we({},"__esModule",{value:!0}),e);var ve=q(ee=>{"use strict";ee.isInteger=e=>typeof e=="number"?Number.isInteger(e):typeof e=="string"&&e.trim()!==""?Number.isInteger(Number(e)):!1;ee.find=(e,r)=>e.nodes.find(t=>t.type===r);ee.exceedsLimit=(e,r,t=1,n)=>n===!1||!ee.isInteger(e)||!ee.isInteger(r)?!1:(Number(r)-Number(e))/Number(t)>=n;ee.escapeNode=(e,r=0,t)=>{let n=e.nodes[r];!n||(t&&n.type===t||n.type==="open"||n.type==="close")&&n.escaped!==!0&&(n.value="\\"+n.value,n.escaped=!0)};ee.encloseBrace=e=>e.type!=="brace"?!1:e.commas>>0+e.ranges>>0===0?(e.invalid=!0,!0):!1;ee.isInvalidBrace=e=>e.type!=="brace"?!1:e.invalid===!0||e.dollar?!0:e.commas>>0+e.ranges>>0===0||e.open!==!0||e.close!==!0?(e.invalid=!0,!0):!1;ee.isOpenOrClose=e=>e.type==="open"||e.type==="close"?!0:e.open===!0||e.close===!0;ee.reduce=e=>e.reduce((r,t)=>(t.type==="text"&&r.push(t.value),t.type==="range"&&(t.type="text"),r),[]);ee.flatten=(...e)=>{let r=[],t=n=>{for(let s=0;s{"use strict";var tt=ve();rt.exports=(e,r={})=>{let t=(n,s={})=>{let i=r.escapeInvalid&&tt.isInvalidBrace(s),a=n.invalid===!0&&r.escapeInvalid===!0,c="";if(n.value)return(i||a)&&tt.isOpenOrClose(n)?"\\"+n.value:n.value;if(n.value)return n.value;if(n.nodes)for(let p of n.nodes)c+=t(p);return c};return t(e)}});var st=q((Vn,nt)=>{"use strict";nt.exports=function(e){return typeof e=="number"?e-e===0:typeof e=="string"&&e.trim()!==""?Number.isFinite?Number.isFinite(+e):isFinite(+e):!1}});var ht=q((Jn,pt)=>{"use strict";var at=st(),le=(e,r,t)=>{if(at(e)===!1)throw new TypeError("toRegexRange: expected the first argument to be a number");if(r===void 0||e===r)return String(e);if(at(r)===!1)throw new TypeError("toRegexRange: expected the second argument to be a number.");let n={relaxZeros:!0,...t};typeof n.strictZeros=="boolean"&&(n.relaxZeros=n.strictZeros===!1);let s=String(n.relaxZeros),i=String(n.shorthand),a=String(n.capture),c=String(n.wrap),p=e+":"+r+"="+s+i+a+c;if(le.cache.hasOwnProperty(p))return le.cache[p].result;let m=Math.min(e,r),h=Math.max(e,r);if(Math.abs(m-h)===1){let y=e+"|"+r;return n.capture?`(${y})`:n.wrap===!1?y:`(?:${y})`}let R=ft(e)||ft(r),f={min:e,max:r,a:m,b:h},$=[],_=[];if(R&&(f.isPadded=R,f.maxLen=String(f.max).length),m<0){let y=h<0?Math.abs(h):1;_=it(y,Math.abs(m),f,n),m=f.a=0}return h>=0&&($=it(m,h,f,n)),f.negatives=_,f.positives=$,f.result=Sr(_,$,n),n.capture===!0?f.result=`(${f.result})`:n.wrap!==!1&&$.length+_.length>1&&(f.result=`(?:${f.result})`),le.cache[p]=f,f.result};function Sr(e,r,t){let n=Pe(e,r,"-",!1,t)||[],s=Pe(r,e,"",!1,t)||[],i=Pe(e,r,"-?",!0,t)||[];return n.concat(i).concat(s).join("|")}function vr(e,r){let t=1,n=1,s=ut(e,t),i=new Set([r]);for(;e<=s&&s<=r;)i.add(s),t+=1,s=ut(e,t);for(s=ct(r+1,n)-1;e1&&c.count.pop(),c.count.push(h.count[0]),c.string=c.pattern+lt(c.count),a=m+1;continue}t.isPadded&&(R=Lr(m,t,n)),h.string=R+h.pattern+lt(h.count),i.push(h),a=m+1,c=h}return i}function Pe(e,r,t,n,s){let i=[];for(let a of e){let{string:c}=a;!n&&!ot(r,"string",c)&&i.push(t+c),n&&ot(r,"string",c)&&i.push(t+c)}return i}function $r(e,r){let t=[];for(let n=0;nr?1:r>e?-1:0}function ot(e,r,t){return e.some(n=>n[r]===t)}function ut(e,r){return Number(String(e).slice(0,-r)+"9".repeat(r))}function ct(e,r){return e-e%Math.pow(10,r)}function lt(e){let[r=0,t=""]=e;return t||r>1?`{${r+(t?","+t:"")}}`:""}function kr(e,r,t){return`[${e}${r-e===1?"":"-"}${r}]`}function ft(e){return/^-?(0+)\d/.test(e)}function Lr(e,r,t){if(!r.isPadded)return e;let n=Math.abs(r.maxLen-String(e).length),s=t.relaxZeros!==!1;switch(n){case 0:return"";case 1:return s?"0?":"0";case 2:return s?"0{0,2}":"00";default:return s?`0{0,${n}}`:`0{${n}}`}}le.cache={};le.clearCache=()=>le.cache={};pt.exports=le});var Ue=q((es,Et)=>{"use strict";var Or=W("util"),At=ht(),dt=e=>e!==null&&typeof e=="object"&&!Array.isArray(e),Nr=e=>r=>e===!0?Number(r):String(r),Me=e=>typeof e=="number"||typeof e=="string"&&e!=="",Ae=e=>Number.isInteger(+e),De=e=>{let r=`${e}`,t=-1;if(r[0]==="-"&&(r=r.slice(1)),r==="0")return!1;for(;r[++t]==="0";);return t>0},Ir=(e,r,t)=>typeof e=="string"||typeof r=="string"?!0:t.stringify===!0,Br=(e,r,t)=>{if(r>0){let n=e[0]==="-"?"-":"";n&&(e=e.slice(1)),e=n+e.padStart(n?r-1:r,"0")}return t===!1?String(e):e},gt=(e,r)=>{let t=e[0]==="-"?"-":"";for(t&&(e=e.slice(1),r--);e.length{e.negatives.sort((a,c)=>ac?1:0),e.positives.sort((a,c)=>ac?1:0);let t=r.capture?"":"?:",n="",s="",i;return e.positives.length&&(n=e.positives.join("|")),e.negatives.length&&(s=`-(${t}${e.negatives.join("|")})`),n&&s?i=`${n}|${s}`:i=n||s,r.wrap?`(${t}${i})`:i},mt=(e,r,t,n)=>{if(t)return At(e,r,{wrap:!1,...n});let s=String.fromCharCode(e);if(e===r)return s;let i=String.fromCharCode(r);return`[${s}-${i}]`},Rt=(e,r,t)=>{if(Array.isArray(e)){let n=t.wrap===!0,s=t.capture?"":"?:";return n?`(${s}${e.join("|")})`:e.join("|")}return At(e,r,t)},yt=(...e)=>new RangeError("Invalid range arguments: "+Or.inspect(...e)),_t=(e,r,t)=>{if(t.strictRanges===!0)throw yt([e,r]);return[]},Mr=(e,r)=>{if(r.strictRanges===!0)throw new TypeError(`Expected step "${e}" to be a number`);return[]},Dr=(e,r,t=1,n={})=>{let s=Number(e),i=Number(r);if(!Number.isInteger(s)||!Number.isInteger(i)){if(n.strictRanges===!0)throw yt([e,r]);return[]}s===0&&(s=0),i===0&&(i=0);let a=s>i,c=String(e),p=String(r),m=String(t);t=Math.max(Math.abs(t),1);let h=De(c)||De(p)||De(m),R=h?Math.max(c.length,p.length,m.length):0,f=h===!1&&Ir(e,r,n)===!1,$=n.transform||Nr(f);if(n.toRegex&&t===1)return mt(gt(e,R),gt(r,R),!0,n);let _={negatives:[],positives:[]},y=T=>_[T<0?"negatives":"positives"].push(Math.abs(T)),E=[],S=0;for(;a?s>=i:s<=i;)n.toRegex===!0&&t>1?y(s):E.push(Br($(s,S),R,f)),s=a?s-t:s+t,S++;return n.toRegex===!0?t>1?Pr(_,n):Rt(E,null,{wrap:!1,...n}):E},Ur=(e,r,t=1,n={})=>{if(!Ae(e)&&e.length>1||!Ae(r)&&r.length>1)return _t(e,r,n);let s=n.transform||(f=>String.fromCharCode(f)),i=`${e}`.charCodeAt(0),a=`${r}`.charCodeAt(0),c=i>a,p=Math.min(i,a),m=Math.max(i,a);if(n.toRegex&&t===1)return mt(p,m,!1,n);let h=[],R=0;for(;c?i>=a:i<=a;)h.push(s(i,R)),i=c?i-t:i+t,R++;return n.toRegex===!0?Rt(h,null,{wrap:!1,options:n}):h},$e=(e,r,t,n={})=>{if(r==null&&Me(e))return[e];if(!Me(e)||!Me(r))return _t(e,r,n);if(typeof t=="function")return $e(e,r,1,{transform:t});if(dt(t))return $e(e,r,0,t);let s={...n};return s.capture===!0&&(s.wrap=!0),t=t||s.step||1,Ae(t)?Ae(e)&&Ae(r)?Dr(e,r,t,s):Ur(e,r,Math.max(Math.abs(t),1),s):t!=null&&!dt(t)?Mr(t,s):$e(e,r,1,t)};Et.exports=$e});var Ct=q((ts,xt)=>{"use strict";var Gr=Ue(),bt=ve(),qr=(e,r={})=>{let t=(n,s={})=>{let i=bt.isInvalidBrace(s),a=n.invalid===!0&&r.escapeInvalid===!0,c=i===!0||a===!0,p=r.escapeInvalid===!0?"\\":"",m="";if(n.isOpen===!0||n.isClose===!0)return p+n.value;if(n.type==="open")return c?p+n.value:"(";if(n.type==="close")return c?p+n.value:")";if(n.type==="comma")return n.prev.type==="comma"?"":c?n.value:"|";if(n.value)return n.value;if(n.nodes&&n.ranges>0){let h=bt.reduce(n.nodes),R=Gr(...h,{...r,wrap:!1,toRegex:!0});if(R.length!==0)return h.length>1&&R.length>1?`(${R})`:R}if(n.nodes)for(let h of n.nodes)m+=t(h,n);return m};return t(e)};xt.exports=qr});var vt=q((rs,St)=>{"use strict";var Kr=Ue(),wt=He(),he=ve(),fe=(e="",r="",t=!1)=>{let n=[];if(e=[].concat(e),r=[].concat(r),!r.length)return e;if(!e.length)return t?he.flatten(r).map(s=>`{${s}}`):r;for(let s of e)if(Array.isArray(s))for(let i of s)n.push(fe(i,r,t));else for(let i of r)t===!0&&typeof i=="string"&&(i=`{${i}}`),n.push(Array.isArray(i)?fe(s,i,t):s+i);return he.flatten(n)},Wr=(e,r={})=>{let t=r.rangeLimit===void 0?1e3:r.rangeLimit,n=(s,i={})=>{s.queue=[];let a=i,c=i.queue;for(;a.type!=="brace"&&a.type!=="root"&&a.parent;)a=a.parent,c=a.queue;if(s.invalid||s.dollar){c.push(fe(c.pop(),wt(s,r)));return}if(s.type==="brace"&&s.invalid!==!0&&s.nodes.length===2){c.push(fe(c.pop(),["{}"]));return}if(s.nodes&&s.ranges>0){let R=he.reduce(s.nodes);if(he.exceedsLimit(...R,r.step,t))throw new RangeError("expanded array length exceeds range limit. Use options.rangeLimit to increase or disable the limit.");let f=Kr(...R,r);f.length===0&&(f=wt(s,r)),c.push(fe(c.pop(),f)),s.nodes=[];return}let p=he.encloseBrace(s),m=s.queue,h=s;for(;h.type!=="brace"&&h.type!=="root"&&h.parent;)h=h.parent,m=h.queue;for(let R=0;R{"use strict";Ht.exports={MAX_LENGTH:1024*64,CHAR_0:"0",CHAR_9:"9",CHAR_UPPERCASE_A:"A",CHAR_LOWERCASE_A:"a",CHAR_UPPERCASE_Z:"Z",CHAR_LOWERCASE_Z:"z",CHAR_LEFT_PARENTHESES:"(",CHAR_RIGHT_PARENTHESES:")",CHAR_ASTERISK:"*",CHAR_AMPERSAND:"&",CHAR_AT:"@",CHAR_BACKSLASH:"\\",CHAR_BACKTICK:"`",CHAR_CARRIAGE_RETURN:"\r",CHAR_CIRCUMFLEX_ACCENT:"^",CHAR_COLON:":",CHAR_COMMA:",",CHAR_DOLLAR:"$",CHAR_DOT:".",CHAR_DOUBLE_QUOTE:'"',CHAR_EQUAL:"=",CHAR_EXCLAMATION_MARK:"!",CHAR_FORM_FEED:"\f",CHAR_FORWARD_SLASH:"/",CHAR_HASH:"#",CHAR_HYPHEN_MINUS:"-",CHAR_LEFT_ANGLE_BRACKET:"<",CHAR_LEFT_CURLY_BRACE:"{",CHAR_LEFT_SQUARE_BRACKET:"[",CHAR_LINE_FEED:` 7 | `,CHAR_NO_BREAK_SPACE:"\xA0",CHAR_PERCENT:"%",CHAR_PLUS:"+",CHAR_QUESTION_MARK:"?",CHAR_RIGHT_ANGLE_BRACKET:">",CHAR_RIGHT_CURLY_BRACE:"}",CHAR_RIGHT_SQUARE_BRACKET:"]",CHAR_SEMICOLON:";",CHAR_SINGLE_QUOTE:"'",CHAR_SPACE:" ",CHAR_TAB:" ",CHAR_UNDERSCORE:"_",CHAR_VERTICAL_LINE:"|",CHAR_ZERO_WIDTH_NOBREAK_SPACE:"\uFEFF"}});var Nt=q((ss,Ot)=>{"use strict";var jr=He(),{MAX_LENGTH:Tt,CHAR_BACKSLASH:Ge,CHAR_BACKTICK:Fr,CHAR_COMMA:Qr,CHAR_DOT:Xr,CHAR_LEFT_PARENTHESES:Zr,CHAR_RIGHT_PARENTHESES:Yr,CHAR_LEFT_CURLY_BRACE:zr,CHAR_RIGHT_CURLY_BRACE:Vr,CHAR_LEFT_SQUARE_BRACKET:kt,CHAR_RIGHT_SQUARE_BRACKET:Lt,CHAR_DOUBLE_QUOTE:Jr,CHAR_SINGLE_QUOTE:en,CHAR_NO_BREAK_SPACE:tn,CHAR_ZERO_WIDTH_NOBREAK_SPACE:rn}=$t(),nn=(e,r={})=>{if(typeof e!="string")throw new TypeError("Expected a string");let t=r||{},n=typeof t.maxLength=="number"?Math.min(Tt,t.maxLength):Tt;if(e.length>n)throw new SyntaxError(`Input length (${e.length}), exceeds max characters (${n})`);let s={type:"root",input:e,nodes:[]},i=[s],a=s,c=s,p=0,m=e.length,h=0,R=0,f,$={},_=()=>e[h++],y=E=>{if(E.type==="text"&&c.type==="dot"&&(c.type="text"),c&&c.type==="text"&&E.type==="text"){c.value+=E.value;return}return a.nodes.push(E),E.parent=a,E.prev=c,c=E,E};for(y({type:"bos"});h0){if(a.ranges>0){a.ranges=0;let E=a.nodes.shift();a.nodes=[E,{type:"text",value:jr(a)}]}y({type:"comma",value:f}),a.commas++;continue}if(f===Xr&&R>0&&a.commas===0){let E=a.nodes;if(R===0||E.length===0){y({type:"text",value:f});continue}if(c.type==="dot"){if(a.range=[],c.value+=f,c.type="range",a.nodes.length!==3&&a.nodes.length!==5){a.invalid=!0,a.ranges=0,c.type="text";continue}a.ranges++,a.args=[];continue}if(c.type==="range"){E.pop();let S=E[E.length-1];S.value+=c.value+f,c=S,a.ranges--;continue}y({type:"dot",value:f});continue}y({type:"text",value:f})}do if(a=i.pop(),a.type!=="root"){a.nodes.forEach(T=>{T.nodes||(T.type==="open"&&(T.isOpen=!0),T.type==="close"&&(T.isClose=!0),T.nodes||(T.type="text"),T.invalid=!0)});let E=i[i.length-1],S=E.nodes.indexOf(a);E.nodes.splice(S,1,...a.nodes)}while(i.length>0);return y({type:"eos"}),s};Ot.exports=nn});var Pt=q((as,Bt)=>{"use strict";var It=He(),sn=Ct(),an=vt(),on=Nt(),Z=(e,r={})=>{let t=[];if(Array.isArray(e))for(let n of e){let s=Z.create(n,r);Array.isArray(s)?t.push(...s):t.push(s)}else t=[].concat(Z.create(e,r));return r&&r.expand===!0&&r.nodupes===!0&&(t=[...new Set(t)]),t};Z.parse=(e,r={})=>on(e,r);Z.stringify=(e,r={})=>It(typeof e=="string"?Z.parse(e,r):e,r);Z.compile=(e,r={})=>(typeof e=="string"&&(e=Z.parse(e,r)),sn(e,r));Z.expand=(e,r={})=>{typeof e=="string"&&(e=Z.parse(e,r));let t=an(e,r);return r.noempty===!0&&(t=t.filter(Boolean)),r.nodupes===!0&&(t=[...new Set(t)]),t};Z.create=(e,r={})=>e===""||e.length<3?[e]:r.expand!==!0?Z.compile(e,r):Z.expand(e,r);Bt.exports=Z});var me=q((is,qt)=>{"use strict";var un=W("path"),se="\\\\/",Mt=`[^${se}]`,ie="\\.",cn="\\+",ln="\\?",Te="\\/",fn="(?=.)",Dt="[^/]",qe=`(?:${Te}|$)`,Ut=`(?:^|${Te})`,Ke=`${ie}{1,2}${qe}`,pn=`(?!${ie})`,hn=`(?!${Ut}${Ke})`,dn=`(?!${ie}{0,1}${qe})`,gn=`(?!${Ke})`,An=`[^.${Te}]`,mn=`${Dt}*?`,Gt={DOT_LITERAL:ie,PLUS_LITERAL:cn,QMARK_LITERAL:ln,SLASH_LITERAL:Te,ONE_CHAR:fn,QMARK:Dt,END_ANCHOR:qe,DOTS_SLASH:Ke,NO_DOT:pn,NO_DOTS:hn,NO_DOT_SLASH:dn,NO_DOTS_SLASH:gn,QMARK_NO_DOT:An,STAR:mn,START_ANCHOR:Ut},Rn={...Gt,SLASH_LITERAL:`[${se}]`,QMARK:Mt,STAR:`${Mt}*?`,DOTS_SLASH:`${ie}{1,2}(?:[${se}]|$)`,NO_DOT:`(?!${ie})`,NO_DOTS:`(?!(?:^|[${se}])${ie}{1,2}(?:[${se}]|$))`,NO_DOT_SLASH:`(?!${ie}{0,1}(?:[${se}]|$))`,NO_DOTS_SLASH:`(?!${ie}{1,2}(?:[${se}]|$))`,QMARK_NO_DOT:`[^.${se}]`,START_ANCHOR:`(?:^|[${se}])`,END_ANCHOR:`(?:[${se}]|$)`},yn={alnum:"a-zA-Z0-9",alpha:"a-zA-Z",ascii:"\\x00-\\x7F",blank:" \\t",cntrl:"\\x00-\\x1F\\x7F",digit:"0-9",graph:"\\x21-\\x7E",lower:"a-z",print:"\\x20-\\x7E ",punct:"\\-!\"#$%&'()\\*+,./:;<=>?@[\\]^_`{|}~",space:" \\t\\r\\n\\v\\f",upper:"A-Z",word:"A-Za-z0-9_",xdigit:"A-Fa-f0-9"};qt.exports={MAX_LENGTH:1024*64,POSIX_REGEX_SOURCE:yn,REGEX_BACKSLASH:/\\(?![*+?^${}(|)[\]])/g,REGEX_NON_SPECIAL_CHARS:/^[^@![\].,$*+?^{}()|\\/]+/,REGEX_SPECIAL_CHARS:/[-*+?.^${}(|)[\]]/,REGEX_SPECIAL_CHARS_BACKREF:/(\\?)((\W)(\3*))/g,REGEX_SPECIAL_CHARS_GLOBAL:/([-*+?.^${}(|)[\]])/g,REGEX_REMOVE_BACKSLASH:/(?:\[.*?[^\\]\]|\\(?=.))/g,REPLACEMENTS:{"***":"*","**/**":"**","**/**/**":"**"},CHAR_0:48,CHAR_9:57,CHAR_UPPERCASE_A:65,CHAR_LOWERCASE_A:97,CHAR_UPPERCASE_Z:90,CHAR_LOWERCASE_Z:122,CHAR_LEFT_PARENTHESES:40,CHAR_RIGHT_PARENTHESES:41,CHAR_ASTERISK:42,CHAR_AMPERSAND:38,CHAR_AT:64,CHAR_BACKWARD_SLASH:92,CHAR_CARRIAGE_RETURN:13,CHAR_CIRCUMFLEX_ACCENT:94,CHAR_COLON:58,CHAR_COMMA:44,CHAR_DOT:46,CHAR_DOUBLE_QUOTE:34,CHAR_EQUAL:61,CHAR_EXCLAMATION_MARK:33,CHAR_FORM_FEED:12,CHAR_FORWARD_SLASH:47,CHAR_GRAVE_ACCENT:96,CHAR_HASH:35,CHAR_HYPHEN_MINUS:45,CHAR_LEFT_ANGLE_BRACKET:60,CHAR_LEFT_CURLY_BRACE:123,CHAR_LEFT_SQUARE_BRACKET:91,CHAR_LINE_FEED:10,CHAR_NO_BREAK_SPACE:160,CHAR_PERCENT:37,CHAR_PLUS:43,CHAR_QUESTION_MARK:63,CHAR_RIGHT_ANGLE_BRACKET:62,CHAR_RIGHT_CURLY_BRACE:125,CHAR_RIGHT_SQUARE_BRACKET:93,CHAR_SEMICOLON:59,CHAR_SINGLE_QUOTE:39,CHAR_SPACE:32,CHAR_TAB:9,CHAR_UNDERSCORE:95,CHAR_VERTICAL_LINE:124,CHAR_ZERO_WIDTH_NOBREAK_SPACE:65279,SEP:un.sep,extglobChars(e){return{"!":{type:"negate",open:"(?:(?!(?:",close:`))${e.STAR})`},"?":{type:"qmark",open:"(?:",close:")?"},"+":{type:"plus",open:"(?:",close:")+"},"*":{type:"star",open:"(?:",close:")*"},"@":{type:"at",open:"(?:",close:")"}}},globChars(e){return e===!0?Rn:Gt}}});var Re=q(Q=>{"use strict";var _n=W("path"),En=process.platform==="win32",{REGEX_BACKSLASH:bn,REGEX_REMOVE_BACKSLASH:xn,REGEX_SPECIAL_CHARS:Cn,REGEX_SPECIAL_CHARS_GLOBAL:wn}=me();Q.isObject=e=>e!==null&&typeof e=="object"&&!Array.isArray(e);Q.hasRegexChars=e=>Cn.test(e);Q.isRegexChar=e=>e.length===1&&Q.hasRegexChars(e);Q.escapeRegex=e=>e.replace(wn,"\\$1");Q.toPosixSlashes=e=>e.replace(bn,"/");Q.removeBackslashes=e=>e.replace(xn,r=>r==="\\"?"":r);Q.supportsLookbehinds=()=>{let e=process.version.slice(1).split(".").map(Number);return e.length===3&&e[0]>=9||e[0]===8&&e[1]>=10};Q.isWindows=e=>e&&typeof e.windows=="boolean"?e.windows:En===!0||_n.sep==="\\";Q.escapeLast=(e,r,t)=>{let n=e.lastIndexOf(r,t);return n===-1?e:e[n-1]==="\\"?Q.escapeLast(e,r,n-1):`${e.slice(0,n)}\\${e.slice(n)}`};Q.removePrefix=(e,r={})=>{let t=e;return t.startsWith("./")&&(t=t.slice(2),r.prefix="./"),t};Q.wrapOutput=(e,r={},t={})=>{let n=t.contains?"":"^",s=t.contains?"":"$",i=`${n}(?:${e})${s}`;return r.negated===!0&&(i=`(?:^(?!${i}).*$)`),i}});var Yt=q((us,Zt)=>{"use strict";var Kt=Re(),{CHAR_ASTERISK:We,CHAR_AT:Sn,CHAR_BACKWARD_SLASH:ye,CHAR_COMMA:vn,CHAR_DOT:je,CHAR_EXCLAMATION_MARK:Fe,CHAR_FORWARD_SLASH:Xt,CHAR_LEFT_CURLY_BRACE:Qe,CHAR_LEFT_PARENTHESES:Xe,CHAR_LEFT_SQUARE_BRACKET:Hn,CHAR_PLUS:$n,CHAR_QUESTION_MARK:Wt,CHAR_RIGHT_CURLY_BRACE:Tn,CHAR_RIGHT_PARENTHESES:jt,CHAR_RIGHT_SQUARE_BRACKET:kn}=me(),Ft=e=>e===Xt||e===ye,Qt=e=>{e.isPrefix!==!0&&(e.depth=e.isGlobstar?1/0:1)},Ln=(e,r)=>{let t=r||{},n=e.length-1,s=t.parts===!0||t.scanToEnd===!0,i=[],a=[],c=[],p=e,m=-1,h=0,R=0,f=!1,$=!1,_=!1,y=!1,E=!1,S=!1,T=!1,L=!1,z=!1,I=!1,re=0,K,g,v={value:"",depth:0,isGlob:!1},k=()=>m>=n,l=()=>p.charCodeAt(m+1),H=()=>(K=g,p.charCodeAt(++m));for(;m0&&(B=p.slice(0,h),p=p.slice(h),R-=h),w&&_===!0&&R>0?(w=p.slice(0,R),o=p.slice(R)):_===!0?(w="",o=p):w=p,w&&w!==""&&w!=="/"&&w!==p&&Ft(w.charCodeAt(w.length-1))&&(w=w.slice(0,-1)),t.unescape===!0&&(o&&(o=Kt.removeBackslashes(o)),w&&T===!0&&(w=Kt.removeBackslashes(w)));let u={prefix:B,input:e,start:h,base:w,glob:o,isBrace:f,isBracket:$,isGlob:_,isExtglob:y,isGlobstar:E,negated:L,negatedExtglob:z};if(t.tokens===!0&&(u.maxDepth=0,Ft(g)||a.push(v),u.tokens=a),t.parts===!0||t.tokens===!0){let P;for(let b=0;b{"use strict";var ke=me(),Y=Re(),{MAX_LENGTH:Le,POSIX_REGEX_SOURCE:On,REGEX_NON_SPECIAL_CHARS:Nn,REGEX_SPECIAL_CHARS_BACKREF:In,REPLACEMENTS:zt}=ke,Bn=(e,r)=>{if(typeof r.expandRange=="function")return r.expandRange(...e,r);e.sort();let t=`[${e.join("-")}]`;try{new RegExp(t)}catch{return e.map(s=>Y.escapeRegex(s)).join("..")}return t},de=(e,r)=>`Missing ${e}: "${r}" - use "\\\\${r}" to match literal characters`,Vt=(e,r)=>{if(typeof e!="string")throw new TypeError("Expected a string");e=zt[e]||e;let t={...r},n=typeof t.maxLength=="number"?Math.min(Le,t.maxLength):Le,s=e.length;if(s>n)throw new SyntaxError(`Input length: ${s}, exceeds maximum allowed length: ${n}`);let i={type:"bos",value:"",output:t.prepend||""},a=[i],c=t.capture?"":"?:",p=Y.isWindows(r),m=ke.globChars(p),h=ke.extglobChars(m),{DOT_LITERAL:R,PLUS_LITERAL:f,SLASH_LITERAL:$,ONE_CHAR:_,DOTS_SLASH:y,NO_DOT:E,NO_DOT_SLASH:S,NO_DOTS_SLASH:T,QMARK:L,QMARK_NO_DOT:z,STAR:I,START_ANCHOR:re}=m,K=A=>`(${c}(?:(?!${re}${A.dot?y:R}).)*?)`,g=t.dot?"":E,v=t.dot?L:z,k=t.bash===!0?K(t):I;t.capture&&(k=`(${k})`),typeof t.noext=="boolean"&&(t.noextglob=t.noext);let l={input:e,index:-1,start:0,dot:t.dot===!0,consumed:"",output:"",prefix:"",backtrack:!1,negated:!1,brackets:0,braces:0,parens:0,quotes:0,globstar:!1,tokens:a};e=Y.removePrefix(e,l),s=e.length;let H=[],w=[],B=[],o=i,u,P=()=>l.index===s-1,b=l.peek=(A=1)=>e[l.index+A],V=l.advance=()=>e[++l.index]||"",J=()=>e.slice(l.index+1),X=(A="",O=0)=>{l.consumed+=A,l.index+=O},Ee=A=>{l.output+=A.output!=null?A.output:A.value,X(A.value)},mr=()=>{let A=1;for(;b()==="!"&&(b(2)!=="("||b(3)==="?");)V(),l.start++,A++;return A%2===0?!1:(l.negated=!0,l.start++,!0)},be=A=>{l[A]++,B.push(A)},oe=A=>{l[A]--,B.pop()},C=A=>{if(o.type==="globstar"){let O=l.braces>0&&(A.type==="comma"||A.type==="brace"),d=A.extglob===!0||H.length&&(A.type==="pipe"||A.type==="paren");A.type!=="slash"&&A.type!=="paren"&&!O&&!d&&(l.output=l.output.slice(0,-o.output.length),o.type="star",o.value="*",o.output=k,l.output+=o.output)}if(H.length&&A.type!=="paren"&&(H[H.length-1].inner+=A.value),(A.value||A.output)&&Ee(A),o&&o.type==="text"&&A.type==="text"){o.value+=A.value,o.output=(o.output||"")+A.value;return}A.prev=o,a.push(A),o=A},xe=(A,O)=>{let d={...h[O],conditions:1,inner:""};d.prev=o,d.parens=l.parens,d.output=l.output;let x=(t.capture?"(":"")+d.open;be("parens"),C({type:A,value:O,output:l.output?"":_}),C({type:"paren",extglob:!0,value:V(),output:x}),H.push(d)},Rr=A=>{let O=A.close+(t.capture?")":""),d;if(A.type==="negate"){let x=k;A.inner&&A.inner.length>1&&A.inner.includes("/")&&(x=K(t)),(x!==k||P()||/^\)+$/.test(J()))&&(O=A.close=`)$))${x}`),A.inner.includes("*")&&(d=J())&&/^\.[^\\/.]+$/.test(d)&&(O=A.close=`)${d})${x})`),A.prev.type==="bos"&&(l.negatedExtglob=!0)}C({type:"paren",extglob:!0,value:u,output:O}),oe("parens")};if(t.fastpaths!==!1&&!/(^[*!]|[/()[\]{}"])/.test(e)){let A=!1,O=e.replace(In,(d,x,M,j,G,Ie)=>j==="\\"?(A=!0,d):j==="?"?x?x+j+(G?L.repeat(G.length):""):Ie===0?v+(G?L.repeat(G.length):""):L.repeat(M.length):j==="."?R.repeat(M.length):j==="*"?x?x+j+(G?k:""):k:x?d:`\\${d}`);return A===!0&&(t.unescape===!0?O=O.replace(/\\/g,""):O=O.replace(/\\+/g,d=>d.length%2===0?"\\\\":d?"\\":"")),O===e&&t.contains===!0?(l.output=e,l):(l.output=Y.wrapOutput(O,l,r),l)}for(;!P();){if(u=V(),u==="\0")continue;if(u==="\\"){let d=b();if(d==="/"&&t.bash!==!0||d==="."||d===";")continue;if(!d){u+="\\",C({type:"text",value:u});continue}let x=/^\\+/.exec(J()),M=0;if(x&&x[0].length>2&&(M=x[0].length,l.index+=M,M%2!==0&&(u+="\\")),t.unescape===!0?u=V():u+=V(),l.brackets===0){C({type:"text",value:u});continue}}if(l.brackets>0&&(u!=="]"||o.value==="["||o.value==="[^")){if(t.posix!==!1&&u===":"){let d=o.value.slice(1);if(d.includes("[")&&(o.posix=!0,d.includes(":"))){let x=o.value.lastIndexOf("["),M=o.value.slice(0,x),j=o.value.slice(x+2),G=On[j];if(G){o.value=M+G,l.backtrack=!0,V(),!i.output&&a.indexOf(o)===1&&(i.output=_);continue}}}(u==="["&&b()!==":"||u==="-"&&b()==="]")&&(u=`\\${u}`),u==="]"&&(o.value==="["||o.value==="[^")&&(u=`\\${u}`),t.posix===!0&&u==="!"&&o.value==="["&&(u="^"),o.value+=u,Ee({value:u});continue}if(l.quotes===1&&u!=='"'){u=Y.escapeRegex(u),o.value+=u,Ee({value:u});continue}if(u==='"'){l.quotes=l.quotes===1?0:1,t.keepQuotes===!0&&C({type:"text",value:u});continue}if(u==="("){be("parens"),C({type:"paren",value:u});continue}if(u===")"){if(l.parens===0&&t.strictBrackets===!0)throw new SyntaxError(de("opening","("));let d=H[H.length-1];if(d&&l.parens===d.parens+1){Rr(H.pop());continue}C({type:"paren",value:u,output:l.parens?")":"\\)"}),oe("parens");continue}if(u==="["){if(t.nobracket===!0||!J().includes("]")){if(t.nobracket!==!0&&t.strictBrackets===!0)throw new SyntaxError(de("closing","]"));u=`\\${u}`}else be("brackets");C({type:"bracket",value:u});continue}if(u==="]"){if(t.nobracket===!0||o&&o.type==="bracket"&&o.value.length===1){C({type:"text",value:u,output:`\\${u}`});continue}if(l.brackets===0){if(t.strictBrackets===!0)throw new SyntaxError(de("opening","["));C({type:"text",value:u,output:`\\${u}`});continue}oe("brackets");let d=o.value.slice(1);if(o.posix!==!0&&d[0]==="^"&&!d.includes("/")&&(u=`/${u}`),o.value+=u,Ee({value:u}),t.literalBrackets===!1||Y.hasRegexChars(d))continue;let x=Y.escapeRegex(o.value);if(l.output=l.output.slice(0,-o.value.length),t.literalBrackets===!0){l.output+=x,o.value=x;continue}o.value=`(${c}${x}|${o.value})`,l.output+=o.value;continue}if(u==="{"&&t.nobrace!==!0){be("braces");let d={type:"brace",value:u,output:"(",outputIndex:l.output.length,tokensIndex:l.tokens.length};w.push(d),C(d);continue}if(u==="}"){let d=w[w.length-1];if(t.nobrace===!0||!d){C({type:"text",value:u,output:u});continue}let x=")";if(d.dots===!0){let M=a.slice(),j=[];for(let G=M.length-1;G>=0&&(a.pop(),M[G].type!=="brace");G--)M[G].type!=="dots"&&j.unshift(M[G].value);x=Bn(j,t),l.backtrack=!0}if(d.comma!==!0&&d.dots!==!0){let M=l.output.slice(0,d.outputIndex),j=l.tokens.slice(d.tokensIndex);d.value=d.output="\\{",u=x="\\}",l.output=M;for(let G of j)l.output+=G.output||G.value}C({type:"brace",value:u,output:x}),oe("braces"),w.pop();continue}if(u==="|"){H.length>0&&H[H.length-1].conditions++,C({type:"text",value:u});continue}if(u===","){let d=u,x=w[w.length-1];x&&B[B.length-1]==="braces"&&(x.comma=!0,d="|"),C({type:"comma",value:u,output:d});continue}if(u==="/"){if(o.type==="dot"&&l.index===l.start+1){l.start=l.index+1,l.consumed="",l.output="",a.pop(),o=i;continue}C({type:"slash",value:u,output:$});continue}if(u==="."){if(l.braces>0&&o.type==="dot"){o.value==="."&&(o.output=R);let d=w[w.length-1];o.type="dots",o.output+=u,o.value+=u,d.dots=!0;continue}if(l.braces+l.parens===0&&o.type!=="bos"&&o.type!=="slash"){C({type:"text",value:u,output:R});continue}C({type:"dot",value:u,output:R});continue}if(u==="?"){if(!(o&&o.value==="(")&&t.noextglob!==!0&&b()==="("&&b(2)!=="?"){xe("qmark",u);continue}if(o&&o.type==="paren"){let x=b(),M=u;if(x==="<"&&!Y.supportsLookbehinds())throw new Error("Node.js v10 or higher is required for regex lookbehinds");(o.value==="("&&!/[!=<:]/.test(x)||x==="<"&&!/<([!=]|\w+>)/.test(J()))&&(M=`\\${u}`),C({type:"text",value:u,output:M});continue}if(t.dot!==!0&&(o.type==="slash"||o.type==="bos")){C({type:"qmark",value:u,output:z});continue}C({type:"qmark",value:u,output:L});continue}if(u==="!"){if(t.noextglob!==!0&&b()==="("&&(b(2)!=="?"||!/[!=<:]/.test(b(3)))){xe("negate",u);continue}if(t.nonegate!==!0&&l.index===0){mr();continue}}if(u==="+"){if(t.noextglob!==!0&&b()==="("&&b(2)!=="?"){xe("plus",u);continue}if(o&&o.value==="("||t.regex===!1){C({type:"plus",value:u,output:f});continue}if(o&&(o.type==="bracket"||o.type==="paren"||o.type==="brace")||l.parens>0){C({type:"plus",value:u});continue}C({type:"plus",value:f});continue}if(u==="@"){if(t.noextglob!==!0&&b()==="("&&b(2)!=="?"){C({type:"at",extglob:!0,value:u,output:""});continue}C({type:"text",value:u});continue}if(u!=="*"){(u==="$"||u==="^")&&(u=`\\${u}`);let d=Nn.exec(J());d&&(u+=d[0],l.index+=d[0].length),C({type:"text",value:u});continue}if(o&&(o.type==="globstar"||o.star===!0)){o.type="star",o.star=!0,o.value+=u,o.output=k,l.backtrack=!0,l.globstar=!0,X(u);continue}let A=J();if(t.noextglob!==!0&&/^\([^?]/.test(A)){xe("star",u);continue}if(o.type==="star"){if(t.noglobstar===!0){X(u);continue}let d=o.prev,x=d.prev,M=d.type==="slash"||d.type==="bos",j=x&&(x.type==="star"||x.type==="globstar");if(t.bash===!0&&(!M||A[0]&&A[0]!=="/")){C({type:"star",value:u,output:""});continue}let G=l.braces>0&&(d.type==="comma"||d.type==="brace"),Ie=H.length&&(d.type==="pipe"||d.type==="paren");if(!M&&d.type!=="paren"&&!G&&!Ie){C({type:"star",value:u,output:""});continue}for(;A.slice(0,3)==="/**";){let Ce=e[l.index+4];if(Ce&&Ce!=="/")break;A=A.slice(3),X("/**",3)}if(d.type==="bos"&&P()){o.type="globstar",o.value+=u,o.output=K(t),l.output=o.output,l.globstar=!0,X(u);continue}if(d.type==="slash"&&d.prev.type!=="bos"&&!j&&P()){l.output=l.output.slice(0,-(d.output+o.output).length),d.output=`(?:${d.output}`,o.type="globstar",o.output=K(t)+(t.strictSlashes?")":"|$)"),o.value+=u,l.globstar=!0,l.output+=d.output+o.output,X(u);continue}if(d.type==="slash"&&d.prev.type!=="bos"&&A[0]==="/"){let Ce=A[1]!==void 0?"|$":"";l.output=l.output.slice(0,-(d.output+o.output).length),d.output=`(?:${d.output}`,o.type="globstar",o.output=`${K(t)}${$}|${$}${Ce})`,o.value+=u,l.output+=d.output+o.output,l.globstar=!0,X(u+V()),C({type:"slash",value:"/",output:""});continue}if(d.type==="bos"&&A[0]==="/"){o.type="globstar",o.value+=u,o.output=`(?:^|${$}|${K(t)}${$})`,l.output=o.output,l.globstar=!0,X(u+V()),C({type:"slash",value:"/",output:""});continue}l.output=l.output.slice(0,-o.output.length),o.type="globstar",o.output=K(t),o.value+=u,l.output+=o.output,l.globstar=!0,X(u);continue}let O={type:"star",value:u,output:k};if(t.bash===!0){O.output=".*?",(o.type==="bos"||o.type==="slash")&&(O.output=g+O.output),C(O);continue}if(o&&(o.type==="bracket"||o.type==="paren")&&t.regex===!0){O.output=u,C(O);continue}(l.index===l.start||o.type==="slash"||o.type==="dot")&&(o.type==="dot"?(l.output+=S,o.output+=S):t.dot===!0?(l.output+=T,o.output+=T):(l.output+=g,o.output+=g),b()!=="*"&&(l.output+=_,o.output+=_)),C(O)}for(;l.brackets>0;){if(t.strictBrackets===!0)throw new SyntaxError(de("closing","]"));l.output=Y.escapeLast(l.output,"["),oe("brackets")}for(;l.parens>0;){if(t.strictBrackets===!0)throw new SyntaxError(de("closing",")"));l.output=Y.escapeLast(l.output,"("),oe("parens")}for(;l.braces>0;){if(t.strictBrackets===!0)throw new SyntaxError(de("closing","}"));l.output=Y.escapeLast(l.output,"{"),oe("braces")}if(t.strictSlashes!==!0&&(o.type==="star"||o.type==="bracket")&&C({type:"maybe_slash",value:"",output:`${$}?`}),l.backtrack===!0){l.output="";for(let A of l.tokens)l.output+=A.output!=null?A.output:A.value,A.suffix&&(l.output+=A.suffix)}return l};Vt.fastpaths=(e,r)=>{let t={...r},n=typeof t.maxLength=="number"?Math.min(Le,t.maxLength):Le,s=e.length;if(s>n)throw new SyntaxError(`Input length: ${s}, exceeds maximum allowed length: ${n}`);e=zt[e]||e;let i=Y.isWindows(r),{DOT_LITERAL:a,SLASH_LITERAL:c,ONE_CHAR:p,DOTS_SLASH:m,NO_DOT:h,NO_DOTS:R,NO_DOTS_SLASH:f,STAR:$,START_ANCHOR:_}=ke.globChars(i),y=t.dot?R:h,E=t.dot?f:h,S=t.capture?"":"?:",T={negated:!1,prefix:""},L=t.bash===!0?".*?":$;t.capture&&(L=`(${L})`);let z=g=>g.noglobstar===!0?L:`(${S}(?:(?!${_}${g.dot?m:a}).)*?)`,I=g=>{switch(g){case"*":return`${y}${p}${L}`;case".*":return`${a}${p}${L}`;case"*.*":return`${y}${L}${a}${p}${L}`;case"*/*":return`${y}${L}${c}${p}${E}${L}`;case"**":return y+z(t);case"**/*":return`(?:${y}${z(t)}${c})?${E}${p}${L}`;case"**/*.*":return`(?:${y}${z(t)}${c})?${E}${L}${a}${p}${L}`;case"**/.*":return`(?:${y}${z(t)}${c})?${a}${p}${L}`;default:{let v=/^(.*?)\.(\w+)$/.exec(g);if(!v)return;let k=I(v[1]);return k?k+a+v[2]:void 0}}},re=Y.removePrefix(e,T),K=I(re);return K&&t.strictSlashes!==!0&&(K+=`${c}?`),K};Jt.exports=Vt});var rr=q((ls,tr)=>{"use strict";var Pn=W("path"),Mn=Yt(),Ze=er(),Ye=Re(),Dn=me(),Un=e=>e&&typeof e=="object"&&!Array.isArray(e),D=(e,r,t=!1)=>{if(Array.isArray(e)){let h=e.map(f=>D(f,r,t));return f=>{for(let $ of h){let _=$(f);if(_)return _}return!1}}let n=Un(e)&&e.tokens&&e.input;if(e===""||typeof e!="string"&&!n)throw new TypeError("Expected pattern to be a non-empty string");let s=r||{},i=Ye.isWindows(r),a=n?D.compileRe(e,r):D.makeRe(e,r,!1,!0),c=a.state;delete a.state;let p=()=>!1;if(s.ignore){let h={...r,ignore:null,onMatch:null,onResult:null};p=D(s.ignore,h,t)}let m=(h,R=!1)=>{let{isMatch:f,match:$,output:_}=D.test(h,a,r,{glob:e,posix:i}),y={glob:e,state:c,regex:a,posix:i,input:h,output:_,match:$,isMatch:f};return typeof s.onResult=="function"&&s.onResult(y),f===!1?(y.isMatch=!1,R?y:!1):p(h)?(typeof s.onIgnore=="function"&&s.onIgnore(y),y.isMatch=!1,R?y:!1):(typeof s.onMatch=="function"&&s.onMatch(y),R?y:!0)};return t&&(m.state=c),m};D.test=(e,r,t,{glob:n,posix:s}={})=>{if(typeof e!="string")throw new TypeError("Expected input to be a string");if(e==="")return{isMatch:!1,output:""};let i=t||{},a=i.format||(s?Ye.toPosixSlashes:null),c=e===n,p=c&&a?a(e):e;return c===!1&&(p=a?a(e):e,c=p===n),(c===!1||i.capture===!0)&&(i.matchBase===!0||i.basename===!0?c=D.matchBase(e,r,t,s):c=r.exec(p)),{isMatch:Boolean(c),match:c,output:p}};D.matchBase=(e,r,t,n=Ye.isWindows(t))=>(r instanceof RegExp?r:D.makeRe(r,t)).test(Pn.basename(e));D.isMatch=(e,r,t)=>D(r,t)(e);D.parse=(e,r)=>Array.isArray(e)?e.map(t=>D.parse(t,r)):Ze(e,{...r,fastpaths:!1});D.scan=(e,r)=>Mn(e,r);D.compileRe=(e,r,t=!1,n=!1)=>{if(t===!0)return e.output;let s=r||{},i=s.contains?"":"^",a=s.contains?"":"$",c=`${i}(?:${e.output})${a}`;e&&e.negated===!0&&(c=`^(?!${c}).*$`);let p=D.toRegex(c,r);return n===!0&&(p.state=e),p};D.makeRe=(e,r={},t=!1,n=!1)=>{if(!e||typeof e!="string")throw new TypeError("Expected a non-empty string");let s={negated:!1,fastpaths:!0};return r.fastpaths!==!1&&(e[0]==="."||e[0]==="*")&&(s.output=Ze.fastpaths(e,r)),s.output||(s=Ze(e,r)),D.compileRe(s,r,t,n)};D.toRegex=(e,r)=>{try{let t=r||{};return new RegExp(e,t.flags||(t.nocase?"i":""))}catch(t){if(r&&r.debug===!0)throw t;return/$^/}};D.constants=Dn;tr.exports=D});var sr=q((fs,nr)=>{"use strict";nr.exports=rr()});var cr=q((ps,ur)=>{"use strict";var ir=W("util"),or=Pt(),ae=sr(),ze=Re(),ar=e=>e===""||e==="./",N=(e,r,t)=>{r=[].concat(r),e=[].concat(e);let n=new Set,s=new Set,i=new Set,a=0,c=h=>{i.add(h.output),t&&t.onResult&&t.onResult(h)};for(let h=0;h!n.has(h));if(t&&m.length===0){if(t.failglob===!0)throw new Error(`No matches found for "${r.join(", ")}"`);if(t.nonull===!0||t.nullglob===!0)return t.unescape?r.map(h=>h.replace(/\\/g,"")):r}return m};N.match=N;N.matcher=(e,r)=>ae(e,r);N.isMatch=(e,r,t)=>ae(r,t)(e);N.any=N.isMatch;N.not=(e,r,t={})=>{r=[].concat(r).map(String);let n=new Set,s=[],a=N(e,r,{...t,onResult:c=>{t.onResult&&t.onResult(c),s.push(c.output)}});for(let c of s)a.includes(c)||n.add(c);return[...n]};N.contains=(e,r,t)=>{if(typeof e!="string")throw new TypeError(`Expected a string: "${ir.inspect(e)}"`);if(Array.isArray(r))return r.some(n=>N.contains(e,n,t));if(typeof r=="string"){if(ar(e)||ar(r))return!1;if(e.includes(r)||e.startsWith("./")&&e.slice(2).includes(r))return!0}return N.isMatch(e,r,{...t,contains:!0})};N.matchKeys=(e,r,t)=>{if(!ze.isObject(e))throw new TypeError("Expected the first argument to be an object");let n=N(Object.keys(e),r,t),s={};for(let i of n)s[i]=e[i];return s};N.some=(e,r,t)=>{let n=[].concat(e);for(let s of[].concat(r)){let i=ae(String(s),t);if(n.some(a=>i(a)))return!0}return!1};N.every=(e,r,t)=>{let n=[].concat(e);for(let s of[].concat(r)){let i=ae(String(s),t);if(!n.every(a=>i(a)))return!1}return!0};N.all=(e,r,t)=>{if(typeof e!="string")throw new TypeError(`Expected a string: "${ir.inspect(e)}"`);return[].concat(r).every(n=>ae(n,t)(e))};N.capture=(e,r,t)=>{let n=ze.isWindows(t),i=ae.makeRe(String(e),{...t,capture:!0}).exec(n?ze.toPosixSlashes(r):r);if(i)return i.slice(1).map(a=>a===void 0?"":a)};N.makeRe=(...e)=>ae.makeRe(...e);N.scan=(...e)=>ae.scan(...e);N.parse=(e,r)=>{let t=[];for(let n of[].concat(e||[]))for(let s of or(String(n),r))t.push(ae.parse(s,r));return t};N.braces=(e,r)=>{if(typeof e!="string")throw new TypeError("Expected a string");return r&&r.nobrace===!0||!/\{.*\}/.test(e)?[e]:or(e,r)};N.braceExpand=(e,r)=>{if(typeof e!="string")throw new TypeError("Expected a string");return N.braces(e,{...r,expand:!0})};ur.exports=N});var fr=q((hs,lr)=>{"use strict";lr.exports=(e,...r)=>new Promise(t=>{t(e(...r))})});var hr=q((ds,Ve)=>{"use strict";var Gn=fr(),pr=e=>{if(e<1)throw new TypeError("Expected `concurrency` to be a number from 1 and up");let r=[],t=0,n=()=>{t--,r.length>0&&r.shift()()},s=(c,p,...m)=>{t++;let h=Gn(c,...m);p(h),h.then(n,n)},i=(c,p,...m)=>{tnew Promise(m=>i(c,m,...p));return Object.defineProperties(a,{activeCount:{get:()=>t},pendingCount:{get:()=>r.length}}),a};Ve.exports=pr;Ve.exports.default=pr});var jn={};Cr(jn,{default:()=>Wn});var Se=W("@yarnpkg/cli"),ne=W("@yarnpkg/core"),et=W("@yarnpkg/core"),ue=W("clipanion"),ce=class extends Se.BaseCommand{constructor(){super(...arguments);this.json=ue.Option.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.production=ue.Option.Boolean("--production",!1,{description:"Only install regular dependencies by omitting dev dependencies"});this.all=ue.Option.Boolean("-A,--all",!1,{description:"Install the entire project"});this.workspaces=ue.Option.Rest()}async execute(){let t=await ne.Configuration.find(this.context.cwd,this.context.plugins),{project:n,workspace:s}=await ne.Project.find(t,this.context.cwd),i=await ne.Cache.find(t);await n.restoreInstallState({restoreResolutions:!1});let a;if(this.all)a=new Set(n.workspaces);else if(this.workspaces.length===0){if(!s)throw new Se.WorkspaceRequiredError(n.cwd,this.context.cwd);a=new Set([s])}else a=new Set(this.workspaces.map(p=>n.getWorkspaceByIdent(et.structUtils.parseIdent(p))));for(let p of a)for(let m of this.production?["dependencies"]:ne.Manifest.hardDependencies)for(let h of p.manifest.getForScope(m).values()){let R=n.tryWorkspaceByDescriptor(h);R!==null&&a.add(R)}for(let p of n.workspaces)a.has(p)?this.production&&p.manifest.devDependencies.clear():(p.manifest.installConfig=p.manifest.installConfig||{},p.manifest.installConfig.selfReferences=!1,p.manifest.dependencies.clear(),p.manifest.devDependencies.clear(),p.manifest.peerDependencies.clear(),p.manifest.scripts.clear());return(await ne.StreamReport.start({configuration:t,json:this.json,stdout:this.context.stdout,includeLogs:!0},async p=>{await n.install({cache:i,report:p,persistProject:!1})})).exitCode()}};ce.paths=[["workspaces","focus"]],ce.usage=ue.Command.Usage({category:"Workspace-related commands",description:"install a single workspace and its dependencies",details:"\n This command will run an install as if the specified workspaces (and all other workspaces they depend on) were the only ones in the project. If no workspaces are explicitly listed, the active one will be assumed.\n\n Note that this command is only very moderately useful when using zero-installs, since the cache will contain all the packages anyway - meaning that the only difference between a full install and a focused install would just be a few extra lines in the `.pnp.cjs` file, at the cost of introducing an extra complexity.\n\n If the `-A,--all` flag is set, the entire project will be installed. Combine with `--production` to replicate the old `yarn install --production`.\n "});var Ne=W("@yarnpkg/cli"),ge=W("@yarnpkg/core"),_e=W("@yarnpkg/core"),F=W("@yarnpkg/core"),gr=W("@yarnpkg/plugin-git"),U=W("clipanion"),Oe=Be(cr()),Ar=Be(hr()),te=Be(W("typanion")),pe=class extends Ne.BaseCommand{constructor(){super(...arguments);this.recursive=U.Option.Boolean("-R,--recursive",!1,{description:"Find packages via dependencies/devDependencies instead of using the workspaces field"});this.from=U.Option.Array("--from",[],{description:"An array of glob pattern idents from which to base any recursion"});this.all=U.Option.Boolean("-A,--all",!1,{description:"Run the command on all workspaces of a project"});this.verbose=U.Option.Boolean("-v,--verbose",!1,{description:"Prefix each output line with the name of the originating workspace"});this.parallel=U.Option.Boolean("-p,--parallel",!1,{description:"Run the commands in parallel"});this.interlaced=U.Option.Boolean("-i,--interlaced",!1,{description:"Print the output of commands in real-time instead of buffering it"});this.jobs=U.Option.String("-j,--jobs",{description:"The maximum number of parallel tasks that the execution will be limited to; or `unlimited`",validator:te.isOneOf([te.isEnum(["unlimited"]),te.applyCascade(te.isNumber(),[te.isInteger(),te.isAtLeast(1)])])});this.topological=U.Option.Boolean("-t,--topological",!1,{description:"Run the command after all workspaces it depends on (regular) have finished"});this.topologicalDev=U.Option.Boolean("--topological-dev",!1,{description:"Run the command after all workspaces it depends on (regular + dev) have finished"});this.include=U.Option.Array("--include",[],{description:"An array of glob pattern idents; only matching workspaces will be traversed"});this.exclude=U.Option.Array("--exclude",[],{description:"An array of glob pattern idents; matching workspaces won't be traversed"});this.publicOnly=U.Option.Boolean("--no-private",{description:"Avoid running the command on private workspaces"});this.since=U.Option.String("--since",{description:"Only include workspaces that have been changed since the specified ref.",tolerateBoolean:!0});this.commandName=U.Option.String();this.args=U.Option.Proxy()}async execute(){let t=await ge.Configuration.find(this.context.cwd,this.context.plugins),{project:n,workspace:s}=await ge.Project.find(t,this.context.cwd);if(!this.all&&!s)throw new Ne.WorkspaceRequiredError(n.cwd,this.context.cwd);await n.restoreInstallState();let i=this.cli.process([this.commandName,...this.args]),a=i.path.length===1&&i.path[0]==="run"&&typeof i.scriptName<"u"?i.scriptName:null;if(i.path.length===0)throw new U.UsageError("Invalid subcommand name for iteration - use the 'run' keyword if you wish to execute a script");let c=this.all?n.topLevelWorkspace:s,p=this.since?Array.from(await gr.gitUtils.fetchChangedWorkspaces({ref:this.since,project:n})):[c,...this.from.length>0?c.getRecursiveWorkspaceChildren():[]],m=g=>Oe.default.isMatch(F.structUtils.stringifyIdent(g.locator),this.from),h=this.from.length>0?p.filter(m):p,R=new Set([...h,...h.map(g=>[...this.recursive?this.since?g.getRecursiveWorkspaceDependents():g.getRecursiveWorkspaceDependencies():g.getRecursiveWorkspaceChildren()]).flat()]),f=[],$=!1;if(a!=null&&a.includes(":")){for(let g of n.workspaces)if(g.manifest.scripts.has(a)&&($=!$,$===!1))break}for(let g of R)a&&!g.manifest.scripts.has(a)&&!$&&!(await ge.scriptUtils.getWorkspaceAccessibleBinaries(g)).has(a)||a===process.env.npm_lifecycle_event&&g.cwd===s.cwd||this.include.length>0&&!Oe.default.isMatch(F.structUtils.stringifyIdent(g.locator),this.include)||this.exclude.length>0&&Oe.default.isMatch(F.structUtils.stringifyIdent(g.locator),this.exclude)||this.publicOnly&&g.manifest.private===!0||f.push(g);let _=this.parallel?this.jobs==="unlimited"?1/0:Number(this.jobs)||Math.ceil(F.nodeUtils.availableParallelism()/2):1,y=_===1?!1:this.parallel,E=y?this.interlaced:!0,S=(0,Ar.default)(_),T=new Map,L=new Set,z=0,I=null,re=!1,K=await _e.StreamReport.start({configuration:t,stdout:this.context.stdout,includePrefix:!1},async g=>{let v=async(k,{commandIndex:l})=>{if(re)return-1;!y&&this.verbose&&l>1&&g.reportSeparator();let H=qn(k,{configuration:t,verbose:this.verbose,commandIndex:l}),[w,B]=dr(g,{prefix:H,interlaced:E}),[o,u]=dr(g,{prefix:H,interlaced:E});try{this.verbose&&g.reportInfo(null,`${H} Process started`);let P=Date.now(),b=await this.cli.run([this.commandName,...this.args],{cwd:k.cwd,stdout:w,stderr:o})||0;w.end(),o.end(),await B,await u;let V=Date.now();if(this.verbose){let J=t.get("enableTimers")?`, completed in ${F.formatUtils.pretty(t,V-P,F.formatUtils.Type.DURATION)}`:"";g.reportInfo(null,`${H} Process exited (exit code ${b})${J}`)}return b===130&&(re=!0,I=b),b}catch(P){throw w.end(),o.end(),await B,await u,P}};for(let k of f)T.set(k.anchoredLocator.locatorHash,k);for(;T.size>0&&!g.hasErrors();){let k=[];for(let[w,B]of T){if(L.has(B.anchoredDescriptor.descriptorHash))continue;let o=!0;if(this.topological||this.topologicalDev){let u=this.topologicalDev?new Map([...B.manifest.dependencies,...B.manifest.devDependencies]):B.manifest.dependencies;for(let P of u.values()){let b=n.tryWorkspaceByDescriptor(P);if(o=b===null||!T.has(b.anchoredLocator.locatorHash),!o)break}}if(!!o&&(L.add(B.anchoredDescriptor.descriptorHash),k.push(S(async()=>{let u=await v(B,{commandIndex:++z});return T.delete(w),L.delete(B.anchoredDescriptor.descriptorHash),u})),!y))break}if(k.length===0){let w=Array.from(T.values()).map(B=>F.structUtils.prettyLocator(t,B.anchoredLocator)).join(", ");g.reportError(_e.MessageName.CYCLIC_DEPENDENCIES,`Dependency cycle detected (${w})`);return}let H=(await Promise.all(k)).find(w=>w!==0);I===null&&(I=typeof H<"u"?1:I),(this.topological||this.topologicalDev)&&typeof H<"u"&&g.reportError(_e.MessageName.UNNAMED,"The command failed for workspaces that are depended upon by other workspaces; can't satisfy the dependency graph")}});return I!==null?I:K.exitCode()}};pe.paths=[["workspaces","foreach"]],pe.usage=U.Command.Usage({category:"Workspace-related commands",description:"run a command on all workspaces",details:"\n This command will run a given sub-command on current and all its descendant workspaces. Various flags can alter the exact behavior of the command:\n\n - If `-p,--parallel` is set, the commands will be ran in parallel; they'll by default be limited to a number of parallel tasks roughly equal to half your core number, but that can be overridden via `-j,--jobs`, or disabled by setting `-j unlimited`.\n\n - If `-p,--parallel` and `-i,--interlaced` are both set, Yarn will print the lines from the output as it receives them. If `-i,--interlaced` wasn't set, it would instead buffer the output from each process and print the resulting buffers only after their source processes have exited.\n\n - If `-t,--topological` is set, Yarn will only run the command after all workspaces that it depends on through the `dependencies` field have successfully finished executing. If `--topological-dev` is set, both the `dependencies` and `devDependencies` fields will be considered when figuring out the wait points.\n\n - If `-A,--all` is set, Yarn will run the command on all the workspaces of a project. By default yarn runs the command only on current and all its descendant workspaces.\n\n - If `-R,--recursive` is set, Yarn will find workspaces to run the command on by recursively evaluating `dependencies` and `devDependencies` fields, instead of looking at the `workspaces` fields.\n\n - If `--from` is set, Yarn will use the packages matching the 'from' glob as the starting point for any recursive search.\n\n - If `--since` is set, Yarn will only run the command on workspaces that have been modified since the specified ref. By default Yarn will use the refs specified by the `changesetBaseRefs` configuration option.\n\n - The command may apply to only some workspaces through the use of `--include` which acts as a whitelist. The `--exclude` flag will do the opposite and will be a list of packages that mustn't execute the script. Both flags accept glob patterns (if valid Idents and supported by [micromatch](https://github.com/micromatch/micromatch)). Make sure to escape the patterns, to prevent your own shell from trying to expand them.\n\n Adding the `-v,--verbose` flag will cause Yarn to print more information; in particular the name of the workspace that generated the output will be printed at the front of each line.\n\n If the command is `run` and the script being run does not exist the child workspace will be skipped without error.\n ",examples:[["Publish current and all descendant packages","yarn workspaces foreach npm publish --tolerate-republish"],["Run build script on current and all descendant packages","yarn workspaces foreach run build"],["Run build script on current and all descendant packages in parallel, building package dependencies first","yarn workspaces foreach -pt run build"],["Run build script on several packages and all their dependencies, building dependencies first","yarn workspaces foreach -ptR --from '{workspace-a,workspace-b}' run build"]]});function dr(e,{prefix:r,interlaced:t}){let n=e.createStreamReporter(r),s=new F.miscUtils.DefaultStream;s.pipe(n,{end:!1}),s.on("finish",()=>{n.end()});let i=new Promise(c=>{n.on("finish",()=>{c(s.active)})});if(t)return[s,i];let a=new F.miscUtils.BufferStream;return a.pipe(s,{end:!1}),a.on("finish",()=>{s.end()}),[a,i]}function qn(e,{configuration:r,commandIndex:t,verbose:n}){if(!n)return null;let i=`[${F.structUtils.stringifyIdent(e.locator)}]:`,a=["#2E86AB","#A23B72","#F18F01","#C73E1D","#CCE2A3"],c=a[t%a.length];return F.formatUtils.pretty(r,i,c)}var Kn={commands:[ce,pe]},Wn=Kn;return wr(jn);})(); 8 | /*! 9 | * fill-range 10 | * 11 | * Copyright (c) 2014-present, Jon Schlinkert. 12 | * Licensed under the MIT License. 13 | */ 14 | /*! 15 | * is-number 16 | * 17 | * Copyright (c) 2014-present, Jon Schlinkert. 18 | * Released under the MIT License. 19 | */ 20 | /*! 21 | * to-regex-range 22 | * 23 | * Copyright (c) 2015-present, Jon Schlinkert. 24 | * Released under the MIT License. 25 | */ 26 | return plugin; 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | nmHoistingLimits: workspaces 3 | 4 | plugins: 5 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs 6 | spec: "@yarnpkg/plugin-interactive-tools" 7 | - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs 8 | spec: "@yarnpkg/plugin-workspace-tools" 9 | 10 | yarnPath: .yarn/releases/yarn-3.6.4.cjs 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2025 ShinMini 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-inner-shadow 2 | 3 | [English](https://www.npmjs.com/package/react-native-inner-shadow) | [한국어](https://github.com/ShinMini/react-native-inner-shadow/blob/main/docs/README.KR.md) 4 | 5 | **react-native-inner-shadow** gives your React Native apps beautiful inset shadows and highlight effects using [React Native Skia](https://shopify.github.io/react-native-skia/docs/getting-started/installation). Create depth in your UI with both solid and gradient backgrounds, plus interactive shadows that respond to touches using [Reanimated](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/getting-started). 6 | 7 | [![npm](https://img.shields.io/npm/v/react-native-inner-shadow.svg)](https://www.npmjs.com/package/react-native-inner-shadow) ![ISC License](https://img.shields.io/npm/l/react-native-inner-shadow.svg) ts-banner 8 | ![downloads](https://img.shields.io/npm/dt/react-native-inner-shadow?style=flat-square) ![downloads](https://img.shields.io/npm/dm/react-native-inner-shadow?style=flat-square) 9 | 10 |
11 | Inner shadow & linear shadow sample 12 | Inner shadow pressable & toggle sample gif 13 |
14 | 15 | ## 🔄 What's New in v2.2.0 16 | 17 | - **Performance boost**: Optimized rendering for smoother animations and less resource usage 18 | - **Reliable layouts**: Fixed size calculations for consistent component dimensions 19 | - **Better border radius**: Individual corner customization with proper shadow rendering 20 | 21 |
22 | More details 23 | 24 | - Added padding to prevent shadow clipping at edges 25 | - Created `useShadowProperties` hook for cleaner, more consistent shadow handling 26 | - Fixed z-index layering for proper component stacking 27 | - Removed unnecessary wrapper elements for better performance 28 | - Improved shadow rendering across all components 29 | - Enhanced gradient handling for smoother color transitions 30 | 31 |
32 | 33 | ## 📋 Table of Contents 34 | 35 | - [react-native-inner-shadow](#react-native-inner-shadow) 36 | - [🔄 What's New in v2.2.0](#-whats-new-in-v220) 37 | - [📋 Table of Contents](#-table-of-contents) 38 | - [🚀 Installation](#-installation) 39 | - [Setup](#setup) 40 | - [🌟 Features](#-features) 41 | - [🧩 Basic Components](#-basic-components) 42 | - [ShadowView](#shadowview) 43 | - [LinearShadowView](#linearshadowview) 44 | - [🔄 Interactive Components](#-interactive-components) 45 | - [ShadowPressable](#shadowpressable) 46 | - [ShadowToggle](#shadowtoggle) 47 | - [🛠 Advanced Usage](#-advanced-usage) 48 | - [Custom Hooks](#custom-hooks) 49 | - [useShadowProperties](#useshadowproperties) 50 | - [useAnimatedOffset](#useanimatedoffset) 51 | - [Border Radius Control](#border-radius-control) 52 | - [Performance Tips](#performance-tips) 53 | - [📚 API Reference](#-api-reference) 54 | - [Constants](#constants) 55 | - [Component Props](#component-props) 56 | - [❓ Troubleshooting](#-troubleshooting) 57 | - [Common Issues](#common-issues) 58 | - [🤝 Contributing](#-contributing) 59 | - [📄 License](#-license) 60 | 61 | ## 🚀 Installation 62 | 63 | ```bash 64 | # Using npm 65 | npm install react-native-inner-shadow @shopify/react-native-skia@next react-native-reanimated 66 | 67 | # Using Yarn 68 | yarn add react-native-inner-shadow @shopify/react-native-skia@next react-native-reanimated 69 | 70 | # Using Expo 71 | npx expo install react-native-inner-shadow @shopify/react-native-skia@next react-native-reanimated 72 | ``` 73 | 74 | ### Setup 75 | 76 | Add Reanimated to your Babel config: 77 | 78 | ```js 79 | // babel.config.js 80 | module.exports = { 81 | presets: [ 82 | // Your existing presets 83 | ], 84 | plugins: [ 85 | // Your existing plugins 86 | 'react-native-reanimated/plugin', 87 | ], 88 | }; 89 | ``` 90 | 91 | For iOS, install pods: 92 | 93 | ```bash 94 | cd ios && pod install && cd .. 95 | ``` 96 | 97 | ## 🌟 Features 98 | 99 | - **Inset shadows**: Create depth effects not possible with React Native's standard shadows 100 | - **Reflected light**: Add subtle highlights for a more realistic 3D appearance 101 | - **Linear gradients**: Combine shadows with beautiful gradient backgrounds 102 | - **Interactive components**: 103 | - Pressable buttons with tactile shadow animations 104 | - Toggle switches with state-dependent shadow effects 105 | - **Custom styling**: 106 | - Per-corner border radius control 107 | - Precise control over shadow properties 108 | - Animated transitions 109 | - **Performance optimized**: 110 | - Smart layout management 111 | - Minimal re-renders 112 | - Efficient canvas usage 113 | 114 | ## 🧩 Basic Components 115 | 116 | ### ShadowView 117 | 118 | The foundation component for creating shadows with solid backgrounds: 119 | 120 | ```tsx 121 | import React from 'react'; 122 | import { View, Text } from 'react-native'; 123 | import { ShadowView } from 'react-native-inner-shadow'; 124 | 125 | export default function Example() { 126 | return ( 127 | 128 | 142 | Inset Shadow 143 | 144 | 145 | ); 146 | } 147 | ``` 148 | 149 | ### LinearShadowView 150 | 151 | For gradient backgrounds with shadows: 152 | 153 | ```tsx 154 | import React from 'react'; 155 | import { View, Text } from 'react-native'; 156 | import { LinearShadowView } from 'react-native-inner-shadow'; 157 | 158 | export default function GradientExample() { 159 | return ( 160 | 161 | 176 | Gradient Shadow 177 | 178 | 179 | ); 180 | } 181 | ``` 182 | 183 | ## 🔄 Interactive Components 184 | 185 | ### ShadowPressable 186 | 187 | Create buttons with satisfying press animations: 188 | 189 | ```tsx 190 | import React from 'react'; 191 | import { View, Text } from 'react-native'; 192 | import { ShadowPressable } from 'react-native-inner-shadow'; 193 | 194 | export default function PressableExample() { 195 | return ( 196 | 197 | console.log('Pressed!')} 210 | > 211 | Press Me 212 | 213 | 214 | ); 215 | } 216 | ``` 217 | 218 | ### ShadowToggle 219 | 220 | Toggle components with state-dependent shadows: 221 | 222 | ```tsx 223 | import React, { useState } from 'react'; 224 | import { View, Text } from 'react-native'; 225 | import { ShadowToggle } from 'react-native-inner-shadow'; 226 | 227 | export default function ToggleExample() { 228 | const [isActive, setIsActive] = useState(false); 229 | 230 | return ( 231 | 232 | setIsActive((prev) => !prev)} 244 | > 245 | 251 | {isActive ? 'ON' : 'OFF'} 252 | 253 | 254 | 255 | ); 256 | } 257 | ``` 258 | 259 | ## 🛠 Advanced Usage 260 | 261 | ### Custom Hooks 262 | 263 | The library provides powerful hooks for advanced customization: 264 | 265 | #### useShadowProperties 266 | 267 | Centralizes shadow configuration for consistent behavior: 268 | 269 | ```tsx 270 | import { useShadowProperties } from 'react-native-inner-shadow'; 271 | 272 | // Inside your component: 273 | const { flatStyle, bgColor, shadowProps, layout, canRenderCanvas, onLayout } = 274 | useShadowProperties({ 275 | propWidth, 276 | propHeight, 277 | style, 278 | inset: true, 279 | shadowOffset: { width: 3, height: 3 }, 280 | shadowBlur: 5, 281 | propsOnLayout: customOnLayoutHandler, 282 | }); 283 | ``` 284 | 285 | #### useAnimatedOffset 286 | 287 | Controls pressable animations with fine-grained control: 288 | 289 | ```tsx 290 | import { useAnimatedOffset } from 'react-native-inner-shadow'; 291 | 292 | // Inside your component: 293 | const { 294 | onPressIn, 295 | onPressOut, 296 | depth, 297 | offset, 298 | reflectedLightOffset, 299 | inset, 300 | blurRadius, 301 | PressedAnimatedStyle, 302 | } = useAnimatedOffset({ 303 | offset: shadowProps.shadowOffset, 304 | reflectedLightOffset: shadowProps.reflectedLightOffset, 305 | blurRadius: shadowProps.shadowBlur, 306 | damping: 0.8, 307 | duration: 150, 308 | onPressIn: customPressInHandler, 309 | onPressOut: customPressOutHandler, 310 | }); 311 | ``` 312 | 313 | ### Border Radius Control 314 | 315 | Customize each corner individually: 316 | 317 | ```tsx 318 | 328 | Custom Corners 329 | 330 | ``` 331 | 332 | ### Performance Tips 333 | 334 | For best performance: 335 | 336 | 1. **Set fixed dimensions** whenever possible 337 | 2. **Memoize components** using React.memo() to prevent unnecessary re-renders 338 | 3. **Use stable keys** when rendering in lists 339 | 4. **Cache styles** instead of generating them on each render 340 | 341 | ```tsx 342 | import React, { memo, useMemo } from 'react'; 343 | import { ShadowView } from 'react-native-inner-shadow'; 344 | 345 | const OptimizedShadowItem = memo(({ title, color }) => { 346 | const styles = useMemo( 347 | () => ({ 348 | container: { 349 | width: 150, 350 | height: 100, 351 | borderRadius: 12, 352 | justifyContent: 'center', 353 | alignItems: 'center', 354 | }, 355 | }), 356 | [] 357 | ); 358 | 359 | return ( 360 | 361 | {title} 362 | 363 | ); 364 | }); 365 | ``` 366 | 367 | ## 📚 API Reference 368 | 369 | ### Constants 370 | 371 | The library provides default values in `src/constants.ts`: 372 | 373 | | Constant | Value | Description | 374 | | ---------------------------- | ----------- | -------------------------------- | 375 | | CANVAS_PADDING | 50 | Space to prevent shadow clipping | 376 | | BACKGROUND_COLOR | '#FFFFFF' | Default background color | 377 | | SHADOW_OFFSET_SCALE | 2.5 | Default shadow offset scale | 378 | | REFLECTED_LIGHT_OFFSET_SCALE | 2 | Default reflection offset scale | 379 | | SHADOW_BLUR | 2 | Default shadow blur radius | 380 | | REFLECTED_LIGHT_BLUR | 3 | Default reflection blur radius | 381 | | SHADOW_COLOR | '#2F2F2FBC' | Default shadow color | 382 | | REFLECTED_LIGHT_COLOR | '#FFFFFF4D' | Default reflection color | 383 | | DAMPING_DURATION | 150 | Animation duration (ms) | 384 | | DAMPING_RATIO | 0.8 | Animation damping ratio | 385 | 386 | ### Component Props 387 | 388 |
389 | ShadowView Props 390 | 391 | | Prop | Type | Default | Description | 392 | | ----------------------- | --------------------------------- | --------------------------- | ---------------------------------------- | 393 | | inset | boolean | false | Makes shadow appear inside the component | 394 | | backgroundColor | string | '#FFFFFF' | Background color | 395 | | shadowColor | string | '#2F2F2FBC' | Shadow color | 396 | | shadowOffset | { width: number, height: number } | { width: 2.5, height: 2.5 } | Shadow position | 397 | | shadowBlur | number | 2 | Shadow blur radius | 398 | | reflectedLightColor | string | '#FFFFFF4D' | Highlight color | 399 | | reflectedLightOffset | { width: number, height: number } | Auto-calculated | Highlight position | 400 | | reflectedLightBlur | number | 3 | Highlight blur radius | 401 | | isReflectedLightEnabled | boolean | true | Whether to show highlights | 402 | | style | ViewStyle | - | React Native style object | 403 | | children | ReactNode | - | Component children | 404 | 405 |
406 | 407 |
408 | LinearShadowView Props (extends ShadowView Props) 409 | 410 | | Prop | Type | Default | Description | 411 | | ------ | -------------------------------------- | -------- | ------------------------ | 412 | | from | 'top' \| 'bottom' \| 'left' \| 'right' | 'top' | Gradient start direction | 413 | | to | 'top' \| 'bottom' \| 'left' \| 'right' | 'bottom' | Gradient end direction | 414 | | colors | Color[] | - | Array of gradient colors | 415 | 416 |
417 | 418 |
419 | ShadowPressable Props 420 | 421 | | Prop | Type | Default | Description | 422 | | ----------------------- | ------- | ------- | ---------------------------------- | 423 | | duration | number | 150 | Animation duration (ms) | 424 | | damping | number | 0.8 | How deeply shadows indent on press | 425 | | isReflectedLightEnabled | boolean | true | Whether to show highlights | 426 | | ...ShadowView Props | - | - | All ShadowView props are supported | 427 | | ...PressableProps | - | - | All React Native Pressable props | 428 | 429 |
430 | 431 |
432 | ShadowToggle Props 433 | 434 | | Prop | Type | Default | Description | 435 | | ------------------------ | ------- | ------- | ---------------------------- | 436 | | isActive | boolean | false | Current toggle state | 437 | | activeColor | string | - | Background color when active | 438 | | ...ShadowPressable Props | - | - | All ShadowPressable props | 439 | 440 |
441 | 442 | ## ❓ Troubleshooting 443 | 444 | ### Common Issues 445 | 446 | 1. **Shadows Not Showing** 447 | 448 | - Make sure width and height are defined (either in style or as props) 449 | - Check border radius values are reasonable for your component size 450 | - Verify shadow colors have opacity (e.g., '#00000066' not '#000000') 451 | 452 | 2. **Dependency Errors** 453 | 454 | - Ensure all three dependencies are properly installed 455 | - Check your babel.config.js includes 'react-native-reanimated/plugin' 456 | - For iOS, run pod install after installation 457 | - For Expo, make sure you're using compatible versions of all packages 458 | 459 | 3. **Performance Problems** 460 | 461 | - Specify fixed dimensions when possible 462 | - Use React.memo() for components in lists 463 | - Check if you're creating new styles on each render 464 | - For scrolling lists, consider virtualizing your list 465 | 466 | 4. **Gradient Not Working** 467 | - Verify your colors array has at least 2 colors 468 | - Check from/to directions are valid ('top', 'bottom', 'left', 'right') 469 | 470 | ## 🤝 Contributing 471 | 472 | Contributions welcome! Check out our [Contributing Guide](https://github.com/ShinMini/react-native-inner-shadow/blob/main/docs/CONTRIBUTING.md) to get started. 473 | 474 | ## 📄 License 475 | 476 | This project is [ISC licensed](https://github.com/ShinMini/react-native-inner-shadow/blob/main/LICENSE). 477 | 478 | --- 479 | 480 | Built by [ShinMini](https://github.com/ShinMini) with ❤️ 481 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:@react-native/babel-preset'], 3 | plugins: ['react-native-reanimated/plugin'], 4 | }; 5 | -------------------------------------------------------------------------------- /docs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | --- 4 | 5 | ## v2.2.0 6 | 7 | - performance: optimize shadow component rendering 8 | - fix: fix shadow component layout size calculation 9 | - fix: fix shadow component border-radius calculation 10 | 11 | fix: component layout and shadow rendering 12 | 13 | - Add canvas padding constant to prevent shadow clipping 14 | - Centralize shadow property handling in useShadowProperties hook 15 | - Fix component positioning and z-index layering 16 | - Clean up redundant style properties and wrapper elements 17 | - Address shadow rendering issues for toggle and pressable components 18 | 19 | feat: refine shadow rendering and default behavior 20 | 21 | Apply consistent code formatting and streamline shadow implementation 22 | with unified reflected light behavior and improved gradient handling. 23 | 24 | --- 25 | 26 | ## v2.1.0 27 | 28 | - performance: optimize shadow component rendering 29 | - fix: fix shadow component layout size calculation 30 | - fix: fix shadow component border-radius calculation 31 | 32 | --- 33 | 34 | ## v2.0.2 35 | 36 | fix: fix onLayout prop in shadow components 37 | refactor: optimize layout size state updates in shadow components 38 | 39 | - Updated ShadowPressable, ShadowToggle, and ShadowView to prevent unnecessary re-renders 40 | - Added comparison logic in setLayoutSize to only update when dimensions change 41 | - Improved performance by using functional state update with previous value check 42 | 43 | --- 44 | 45 | ## v.2.0.1 46 | 47 | chore: bump package version to 2.0.1 48 | 49 | - Updated package.json version number 50 | - Fixed image link in README and README.KR by adding .gif extension 51 | 52 | ## v.2.0.0 53 | 54 | - feat!: can configure any border corner to any shadow component 55 | - Replaced Group and Rect with RoundedRect in CornerRadii 56 | - Removed makeRoundedRectPath utility function from utils.ts 57 | - Updated CornerRadii to use InputRRect type from react-native-skia 58 | - Simplified corner radius calculation and rendering 59 | - Improved component compatibility with Skia's RoundedRect 60 | 61 | refactor!: simplify pressable component by removing complicated code by replacing and **deprecating** `shadowSpace`. 62 | 63 | - `shadowSpace` now `shadowSpace` replaced with `style.padding` properties 64 | 65 | refactor: adjust damping calculation in useAnimatedOffset hook 66 | 67 | - Simplified damping calculation by using a fixed 0.5 multiplier 68 | - Removed custom damping prop usage in offset interpolation 69 | - Streamlined animation offset calculation for improved consistency 70 | 71 | refactor: enhance shadow types and improve type documentation 72 | 73 | - Expanded and refined shadow type definitions in `types.ts` 74 | - Added comprehensive documentation for shadow properties 75 | - Introduced `ShadowProps` as a base type for shadow-related components 76 | - Updated constants and utility functions to align with new type definitions 77 | - Improved type clarity and added more detailed default value descriptions 78 | 79 | feat: unify `ShadowPressable` and `ShadowToggle` with linear gradient support 80 | 81 | - Introduced UnifiedShadowPressable and UnifiedShadowToggle components 82 | - Added support for linear gradient backgrounds 83 | - Created useAnimatedOffset hook to centralize animation logic 84 | - Improved layout measurement and rendering 85 | - Added LinearShadowPressable and LinearShadowToggle variants 86 | 87 | feat: export `LinearShadowPressable` and `LinearShadowToggle` components 88 | 89 | - Added exports for LinearShadowPressable and LinearShadowToggle from respective component files 90 | - Expanded component export options in index.ts 91 | - Consistent with previous linear shadow component additions 92 | 93 | refactor(chore): remove unused `SHADOW_SPACE` constant 94 | 95 | Cleaned up constants by removing the unused `SHADOW_SPACE` constant from the constants file, maintaining a lean and focused set of shadow-related constants. 96 | 97 | docs: enhance type documentation with comprehensive comments and examples 98 | 99 | - Added detailed JSDoc comments for all types in `types.ts` 100 | - Provided clear explanations for shadow-related type definitions 101 | - Included usage examples for each type 102 | - Improved type documentation with remarks, default values, and type descriptions 103 | - Enhanced readability and understanding of shadow component types 104 | 105 | --- 106 | 107 | ## v1.3.1 108 | 109 | ### Added or Changed 110 | 111 | - adjust blur scale for pressable components 112 | 113 | --- 114 | 115 | ## v1.3.0 116 | 117 | ### Added or Changed 118 | 119 | - refactor constants prefix by removing `DEFAULT_` from the name 120 | 121 | ### Removed 122 | 123 | - exclude constants exports from `index.ts` file 124 | - remove not used `createStyle` function in `utils.ts` 125 | - deprecated `initialDepth` prop in `ShadowPressable` and `ShadowToggle` components which is now replaced by `shadowOffset` prop 126 | 127 | --- 128 | 129 | ## v1.2.5 130 | 131 | ### Added or Changed 132 | 133 | - Change license; 134 | - Add simplified project cover image 135 | 136 | ### Removed 137 | 138 | - Some constants from acknowledgements I no longer use 139 | -------------------------------------------------------------------------------- /docs/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | shinmini@shinmini.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are always welcome, no matter how large or small! 4 | 5 | We want this community to be friendly and respectful to each other. Please follow it in all your interactions with the project. Before contributing, please read the [code of conduct](./CODE_OF_CONDUCT.md). 6 | 7 | ## Development workflow 8 | 9 | This project is a monorepo managed using [Yarn workspaces](https://yarnpkg.com/features/workspaces). It contains the following packages: 10 | 11 | - The library package in the root directory. 12 | - An example app in the `example/` directory. 13 | 14 | To get started with the project, run `yarn` in the root directory to install the required dependencies for each package: 15 | 16 | ```sh 17 | yarn 18 | ``` 19 | 20 | > Since the project relies on Yarn workspaces, you cannot use [`npm`](https://github.com/npm/cli) for development. 21 | 22 | The [example app](/example/) demonstrates usage of the library. You need to run it to test any changes you make. 23 | 24 | It is configured to use the local version of the library, so any changes you make to the library's source code will be reflected in the example app. Changes to the library's JavaScript code will be reflected in the example app without a rebuild, but native code changes will require a rebuild of the example app. 25 | 26 | You can use various commands from the root directory to work with the project. 27 | 28 | To start the packager: 29 | 30 | ```sh 31 | yarn example start 32 | ``` 33 | 34 | To run the example app on Android: 35 | 36 | ```sh 37 | yarn example android 38 | ``` 39 | 40 | To run the example app on iOS: 41 | 42 | ```sh 43 | yarn example ios 44 | ``` 45 | 46 | To confirm that the app is running with the new architecture, you can check the Metro logs for a message like this: 47 | 48 | ```sh 49 | Running "ShinminiTestLibraryExample" with {"fabric":true,"initialProps":{"concurrentRoot":true},"rootTag":1} 50 | ``` 51 | 52 | Note the `"fabric":true` and `"concurrentRoot":true` properties. 53 | 54 | To run the example app on Web: 55 | 56 | ```sh 57 | yarn example web 58 | ``` 59 | 60 | Make sure your code passes TypeScript and ESLint. Run the following to verify: 61 | 62 | ```sh 63 | yarn typecheck 64 | yarn lint 65 | ``` 66 | 67 | To fix formatting errors, run the following: 68 | 69 | ```sh 70 | yarn lint --fix 71 | ``` 72 | 73 | Remember to add tests for your change if possible. Run the unit tests by: 74 | 75 | ```sh 76 | yarn test 77 | ``` 78 | 79 | ### Commit message convention 80 | 81 | We follow the [conventional commits specification](https://www.conventionalcommits.org/en) for our commit messages: 82 | 83 | - `fix`: bug fixes, e.g. fix crash due to deprecated method. 84 | - `feat`: new features, e.g. add new method to the module. 85 | - `refactor`: code refactor, e.g. migrate from class components to hooks. 86 | - `docs`: changes into documentation, e.g. add usage example for the module.. 87 | - `test`: adding or updating tests, e.g. add integration tests using detox. 88 | - `chore`: tooling changes, e.g. change CI config. 89 | 90 | Our pre-commit hooks verify that your commit message matches this format when committing. 91 | 92 | ### Linting and tests 93 | 94 | [ESLint](https://eslint.org/), [Prettier](https://prettier.io/), [TypeScript](https://www.typescriptlang.org/) 95 | 96 | We use [TypeScript](https://www.typescriptlang.org/) for type checking, [ESLint](https://eslint.org/) with [Prettier](https://prettier.io/) for linting and formatting the code, and [Jest](https://jestjs.io/) for testing. 97 | 98 | Our pre-commit hooks verify that the linter and tests pass when committing. 99 | 100 | ### Publishing to npm 101 | 102 | We use [release-it](https://github.com/release-it/release-it) to make it easier to publish new versions. It handles common tasks like bumping version based on semver, creating tags and releases etc. 103 | 104 | To publish new versions, run the following: 105 | 106 | ```sh 107 | yarn release 108 | ``` 109 | 110 | ### Scripts 111 | 112 | The `package.json` file contains various scripts for common tasks: 113 | 114 | - `yarn`: setup project by installing dependencies. 115 | - `yarn typecheck`: type-check files with TypeScript. 116 | - `yarn lint`: lint files with ESLint. 117 | - `yarn test`: run unit tests with Jest. 118 | - `yarn example start`: start the Metro server for the example app. 119 | - `yarn example android`: run the example app on Android. 120 | - `yarn example ios`: run the example app on iOS. 121 | 122 | ### Sending a pull request 123 | 124 | > **Working on your first pull request?** You can learn how from this _free_ series: [How to Contribute to an Open Source Project on GitHub](https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github). 125 | 126 | When you're sending a pull request: 127 | 128 | - Prefer small pull requests focused on one change. 129 | - Verify that linters and tests are passing. 130 | - Review the documentation to make sure it looks good. 131 | - Follow the pull request template when opening a pull request. 132 | - For pull requests that change the API or implementation, discuss with maintainers first by opening an issue. 133 | -------------------------------------------------------------------------------- /docs/README.KR.md: -------------------------------------------------------------------------------- 1 | # react-native-inner-shadow 2 | 3 | [English](https://www.npmjs.com/package/react-native-inner-shadow) | [한국어](https://github.com/ShinMini/react-native-inner-shadow/blob/main/docs/README.KR.md) 4 | 5 | **react-native-inner-shadow**는 [React Native Skia](https://shopify.github.io/react-native-skia/)를 활용해 **인셋(내부) 그림자**와 **반사광 효과**를 구현한 라이브러리입니다. [Reanimated](https://docs.swmansion.com/react-native-reanimated/)를 사용한 애니메이션 효과로 단색 배경과 그라데이션 배경 모두에서 자연스러운 그림자와 인터랙티브한 반응을 만들 수 있습니다. 6 | 7 | [![npm](https://img.shields.io/npm/v/react-native-inner-shadow.svg)](https://www.npmjs.com/package/react-native-inner-shadow) ![ISC License](https://img.shields.io/npm/l/react-native-inner-shadow.svg) ts-banner 8 | ![downloads](https://img.shields.io/npm/dm/react-native-inner-shadow?style=flat-square) ![downloads](https://img.shields.io/npm/dw/react-native-inner-shadow?style=flat-square) 9 | 10 |
11 | 내부 그림자 및 선형 그림자 샘플 12 | 내부 그림자 프레스 및 토글 샘플 gif 13 |
14 | 15 | ## 🔄 v2.2.0 업데이트 내용 16 | 17 | - **성능 최적화**: 더 부드러운 애니메이션과 리소스 사용량 감소 18 | - **레이아웃 개선**: 정확한 컴포넌트 크기 계산 방식 수정 19 | - **보더 래디어스 개선**: 각 모서리를 개별적으로 설정 가능 20 | 21 |
22 | 자세히 보기 23 | 24 | - 그림자가 잘리는 것을 방지하기 위한 패딩 추가 25 | - `useShadowProperties` 훅 개발로 일관된 그림자 처리 26 | - z-index 레이어링 수정으로 컴포넌트 겹침 문제 해결 27 | - 불필요한 래퍼 요소 제거로 성능 개선 28 | - 토글 및 프레서블 컴포넌트의 그림자 렌더링 문제 수정 29 | - 반사광 처리 및 그라데이션 표현 향상 30 | 31 |
32 | 33 | ## 📋 목차 34 | 35 | - [react-native-inner-shadow](#react-native-inner-shadow) 36 | - [🔄 v2.2.0 업데이트 내용](#-v220-업데이트-내용) 37 | - [📋 목차](#-목차) 38 | - [🚀 설치](#-설치) 39 | - [설정](#설정) 40 | - [🌟 특징](#-특징) 41 | - [🧩 기본 컴포넌트](#-기본-컴포넌트) 42 | - [ShadowView](#shadowview) 43 | - [LinearShadowView](#linearshadowview) 44 | - [🔄 인터랙티브 컴포넌트](#-인터랙티브-컴포넌트) 45 | - [ShadowPressable](#shadowpressable) 46 | - [ShadowToggle](#shadowtoggle) 47 | - [🛠 고급 사용법](#-고급-사용법) 48 | - [커스텀 훅](#커스텀-훅) 49 | - [useShadowProperties](#useshadowproperties) 50 | - [useAnimatedOffset](#useanimatedoffset) 51 | - [보더 래디어스 컨트롤](#보더-래디어스-컨트롤) 52 | - [성능 최적화 팁](#성능-최적화-팁) 53 | - [📚 API 참조](#-api-참조) 54 | - [상수](#상수) 55 | - [컴포넌트 Props](#컴포넌트-props) 56 | - [❓ 문제 해결](#-문제-해결) 57 | - [일반적인 문제](#일반적인-문제) 58 | - [🤝 기여하기](#-기여하기) 59 | - [📄 라이센스](#-라이센스) 60 | 61 | ## 🚀 설치 62 | 63 | ```bash 64 | # npm 사용 65 | npm install react-native-inner-shadow @shopify/react-native-skia@next react-native-reanimated 66 | 67 | # Yarn 사용 68 | yarn add react-native-inner-shadow @shopify/react-native-skia@next react-native-reanimated 69 | 70 | # Expo 사용 71 | npx expo install react-native-inner-shadow @shopify/react-native-skia@next react-native-reanimated 72 | ``` 73 | 74 | ### 설정 75 | 76 | Babel 설정에 Reanimated 플러그인 추가: 77 | 78 | ```js 79 | // babel.config.js 80 | module.exports = { 81 | presets: [ 82 | // 기존 프리셋 83 | ], 84 | plugins: [ 85 | // 기존 플러그인 86 | 'react-native-reanimated/plugin', 87 | ], 88 | }; 89 | ``` 90 | 91 | iOS의 경우 pods 설치: 92 | 93 | ```bash 94 | cd ios && pod install && cd .. 95 | ``` 96 | 97 | ## 🌟 특징 98 | 99 | - **인셋 그림자**: 리액트 네이티브에서 기본적으로 제공하지 않는 내부 그림자 효과 100 | - **반사광 효과**: 더 사실적인 3D 느낌을 위한 미묘한 하이라이트 101 | - **선형 그라데이션**: 아름다운 그라데이션 배경과 그림자 조합 102 | - **인터랙티브 컴포넌트**: 103 | - 그림자 애니메이션이 있는 프레서블 버튼 104 | - 상태에 따라 변하는 토글 스위치 105 | - **커스텀 스타일링**: 106 | - 각 모서리별 보더 래디어스 제어 107 | - 그림자 속성에 대한 정밀한 조절 108 | - 애니메이션 전환 효과 109 | - **성능 최적화**: 110 | - 스마트 레이아웃 관리 111 | - 최소한의 리렌더링 112 | - 효율적인 캔버스 활용 113 | 114 | ## 🧩 기본 컴포넌트 115 | 116 | ### ShadowView 117 | 118 | 단색 배경에 그림자를 생성하는 기본 컴포넌트: 119 | 120 | ```tsx 121 | import React from 'react'; 122 | import { View, Text } from 'react-native'; 123 | import { ShadowView } from 'react-native-inner-shadow'; 124 | 125 | export default function Example() { 126 | return ( 127 | 128 | 142 | 내부 그림자 143 | 144 | 145 | ); 146 | } 147 | ``` 148 | 149 | ### LinearShadowView 150 | 151 | 그라데이션 배경과 그림자를 결합: 152 | 153 | ```tsx 154 | import React from 'react'; 155 | import { View, Text } from 'react-native'; 156 | import { LinearShadowView } from 'react-native-inner-shadow'; 157 | 158 | export default function GradientExample() { 159 | return ( 160 | 161 | 176 | 그라데이션 그림자 177 | 178 | 179 | ); 180 | } 181 | ``` 182 | 183 | ## 🔄 인터랙티브 컴포넌트 184 | 185 | ### ShadowPressable 186 | 187 | 만족스러운 누름 애니메이션이 있는 버튼 생성: 188 | 189 | ```tsx 190 | import React from 'react'; 191 | import { View, Text } from 'react-native'; 192 | import { ShadowPressable } from 'react-native-inner-shadow'; 193 | 194 | export default function PressableExample() { 195 | return ( 196 | 197 | console.log('누름!')} 210 | > 211 | 눌러보세요 212 | 213 | 214 | ); 215 | } 216 | ``` 217 | 218 | ### ShadowToggle 219 | 220 | 상태에 따라 그림자가 변하는 토글 컴포넌트: 221 | 222 | ```tsx 223 | import React, { useState } from 'react'; 224 | import { View, Text } from 'react-native'; 225 | import { ShadowToggle } from 'react-native-inner-shadow'; 226 | 227 | export default function ToggleExample() { 228 | const [isActive, setIsActive] = useState(false); 229 | 230 | return ( 231 | 232 | setIsActive(prev => !prev)} 244 | > 245 | 249 | {isActive ? 'ON' : 'OFF'} 250 | 251 | 252 | 253 | ); 254 | } 255 | ``` 256 | 257 | ## 🛠 고급 사용법 258 | 259 | ### 커스텀 훅 260 | 261 | 라이브러리는 고급 사용자를 위한 강력한 훅을 제공합니다: 262 | 263 | #### useShadowProperties 264 | 265 | 일관된 그림자 설정을 위한 중앙 집중식 구성: 266 | 267 | ```tsx 268 | import { useShadowProperties } from 'react-native-inner-shadow'; 269 | 270 | // 컴포넌트 내에서: 271 | const { 272 | flatStyle, 273 | bgColor, 274 | shadowProps, 275 | layout, 276 | canRenderCanvas, 277 | onLayout 278 | } = useShadowProperties({ 279 | propWidth, 280 | propHeight, 281 | style, 282 | inset: true, 283 | shadowOffset: { width: 3, height: 3 }, 284 | shadowBlur: 5, 285 | propsOnLayout: 커스텀OnLayoutHandler 286 | }); 287 | ``` 288 | 289 | #### useAnimatedOffset 290 | 291 | 세밀한 제어로 프레서블 애니메이션 조작: 292 | 293 | ```tsx 294 | import { useAnimatedOffset } from 'react-native-inner-shadow'; 295 | 296 | // 컴포넌트 내에서: 297 | const { 298 | onPressIn, 299 | onPressOut, 300 | depth, 301 | offset, 302 | reflectedLightOffset, 303 | inset, 304 | blurRadius, 305 | PressedAnimatedStyle 306 | } = useAnimatedOffset({ 307 | offset: shadowProps.shadowOffset, 308 | reflectedLightOffset: shadowProps.reflectedLightOffset, 309 | blurRadius: shadowProps.shadowBlur, 310 | damping: 0.8, 311 | duration: 150, 312 | onPressIn: 커스텀PressInHandler, 313 | onPressOut: 커스텀PressOutHandler 314 | }); 315 | ``` 316 | 317 | ### 보더 래디어스 컨트롤 318 | 319 | 각 모서리를 개별적으로 사용자 정의: 320 | 321 | ```tsx 322 | 332 | 커스텀 모서리 333 | 334 | ``` 335 | 336 | ### 성능 최적화 팁 337 | 338 | 최상의 성능을 위해: 339 | 340 | 1. **고정 치수 설정**: 가능하면 항상 너비와 높이를 명시적으로 지정하세요 341 | 2. **컴포넌트 메모이제이션**: React.memo()를 사용하여 불필요한 리렌더링 방지 342 | 3. **안정적인 키 사용**: 리스트에서 렌더링할 때 고유하고 안정적인 키 사용 343 | 4. **스타일 캐싱**: 매 렌더링마다 새로운 스타일을 생성하지 마세요 344 | 345 | ```tsx 346 | import React, { memo, useMemo } from 'react'; 347 | import { ShadowView } from 'react-native-inner-shadow'; 348 | 349 | const OptimizedShadowItem = memo(({ title, color }) => { 350 | const styles = useMemo(() => ({ 351 | container: { 352 | width: 150, 353 | height: 100, 354 | borderRadius: 12, 355 | justifyContent: 'center', 356 | alignItems: 'center', 357 | } 358 | }), []); 359 | 360 | return ( 361 | 366 | {title} 367 | 368 | ); 369 | }); 370 | ``` 371 | 372 | ## 📚 API 참조 373 | 374 | ### 상수 375 | 376 | 라이브러리는 `src/constants.ts`에 기본값을 제공합니다: 377 | 378 | | 상수 | 값 | 설명 | 379 | |----------|-------|-------------| 380 | | CANVAS_PADDING | 50 | 그림자 잘림 방지를 위한 여백 | 381 | | BACKGROUND_COLOR | '#FFFFFF' | 기본 배경색 | 382 | | SHADOW_OFFSET_SCALE | 2.5 | 기본 그림자 오프셋 스케일 | 383 | | REFLECTED_LIGHT_OFFSET_SCALE | 2 | 기본 반사광 오프셋 스케일 | 384 | | SHADOW_BLUR | 2 | 기본 그림자 블러 반경 | 385 | | REFLECTED_LIGHT_BLUR | 3 | 기본 반사광 블러 반경 | 386 | | SHADOW_COLOR | '#2F2F2FBC' | 기본 그림자 색상 | 387 | | REFLECTED_LIGHT_COLOR | '#FFFFFF4D' | 기본 반사광 색상 | 388 | | DAMPING_DURATION | 150 | 애니메이션 지속 시간(ms) | 389 | | DAMPING_RATIO | 0.8 | 애니메이션 감쇠 비율 | 390 | 391 | ### 컴포넌트 Props 392 | 393 |
394 | ShadowView Props 395 | 396 | | Prop | 타입 | 기본값 | 설명 | 397 | |------|------|---------|-------------| 398 | | inset | boolean | false | 그림자를 컴포넌트 내부에 표시 | 399 | | backgroundColor | string | '#FFFFFF' | 배경색 | 400 | | shadowColor | string | '#2F2F2FBC' | 그림자 색상 | 401 | | shadowOffset | { width: number, height: number } | { width: 2.5, height: 2.5 } | 그림자 위치 | 402 | | shadowBlur | number | 2 | 그림자 블러 반경 | 403 | | reflectedLightColor | string | '#FFFFFF4D' | 하이라이트 색상 | 404 | | reflectedLightOffset | { width: number, height: number } | 자동 계산 | 하이라이트 위치 | 405 | | reflectedLightBlur | number | 3 | 하이라이트 블러 반경 | 406 | | isReflectedLightEnabled | boolean | true | 하이라이트 표시 여부 | 407 | | style | ViewStyle | - | React Native 스타일 객체 | 408 | | children | ReactNode | - | 자식 컴포넌트 | 409 | 410 |
411 | 412 |
413 | LinearShadowView Props (ShadowView Props 확장) 414 | 415 | | Prop | 타입 | 기본값 | 설명 | 416 | |------|------|---------|-------------| 417 | | from | 'top' \| 'bottom' \| 'left' \| 'right' | 'top' | 그라데이션 시작 방향 | 418 | | to | 'top' \| 'bottom' \| 'left' \| 'right' | 'bottom' | 그라데이션 끝 방향 | 419 | | colors | Color[] | - | 그라데이션 색상 배열 | 420 | 421 |
422 | 423 |
424 | ShadowPressable Props 425 | 426 | | Prop | 타입 | 기본값 | 설명 | 427 | |------|------|---------|-------------| 428 | | duration | number | 150 | 애니메이션 지속 시간(ms) | 429 | | damping | number | 0.8 | 누를 때 그림자가 들어가는 정도 | 430 | | isReflectedLightEnabled | boolean | true | 하이라이트 표시 여부 | 431 | | ...ShadowView Props | - | - | 모든 ShadowView 속성 지원 | 432 | | ...PressableProps | - | - | 모든 React Native Pressable 속성 | 433 | 434 |
435 | 436 |
437 | ShadowToggle Props 438 | 439 | | Prop | 타입 | 기본값 | 설명 | 440 | |------|------|---------|-------------| 441 | | isActive | boolean | false | 현재 토글 상태 | 442 | | activeColor | string | - | 활성 상태일 때 배경색 | 443 | | ...ShadowPressable Props | - | - | 모든 ShadowPressable 속성 | 444 | 445 |
446 | 447 | ## ❓ 문제 해결 448 | 449 | ### 일반적인 문제 450 | 451 | 1. **그림자가 보이지 않는 경우** 452 | - 너비와 높이가 정의되어 있는지 확인 (스타일 또는 속성으로) 453 | - 보더 래디어스 값이 컴포넌트 크기에 적절한지 확인 454 | - 그림자 색상에 투명도가 있는지 확인 (예: '#00000066', '#000000' 아님) 455 | 456 | 2. **의존성 오류** 457 | - 세 가지 의존성이 모두 제대로 설치되었는지 확인 458 | - babel.config.js에 'react-native-reanimated/plugin'이 포함되어 있는지 확인 459 | - iOS의 경우 설치 후 pod install 실행 460 | - Expo의 경우 모든 패키지의 호환 버전 확인 461 | 462 | 3. **성능 문제** 463 | - 가능한 경우 고정 치수 지정 464 | - 리스트의 컴포넌트에 React.memo() 사용 465 | - 매 렌더링마다 새 스타일을 생성하는지 확인 466 | - 스크롤 리스트의 경우 가상화된 리스트 고려 467 | 468 | 4. **그라데이션이 작동하지 않는 경우** 469 | - colors 배열에 최소 2개의 색상이 있는지 확인 470 | - from/to 방향이 유효한지 확인('top', 'bottom', 'left', 'right') 471 | 472 | ## 🤝 기여하기 473 | 474 | 기여는 언제나 환영합니다! 시작하려면 [기여 가이드](https://github.com/ShinMini/react-native-inner-shadow/blob/main/docs/CONTRIBUTING.md)를 확인하세요. 475 | 476 | ## 📄 라이센스 477 | 478 | 이 프로젝트는 [ISC 라이센스](https://github.com/ShinMini/react-native-inner-shadow/blob/main/LICENSE)입니다. 479 | 480 | --- 481 | 482 | [ShinMini](https://github.com/ShinMini)가 ❤️로 만들었습니다. 483 | -------------------------------------------------------------------------------- /docs/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 5.1.x | :white_check_mark: | 11 | | 5.0.x | :x: | 12 | | 4.0.x | :white_check_mark: | 13 | | < 4.0 | :x: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | Use this section to tell people how to report a vulnerability. 18 | 19 | Tell them where to go, how often they can expect to get an update on a 20 | reported vulnerability, what to expect if the vulnerability is accepted or 21 | declined, etc. 22 | -------------------------------------------------------------------------------- /docs/imgs/rn-inner-shadow-gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShinMini/react-native-inner-shadow/9cb32669daf21e12b9e1684483540f08e24ebfee/docs/imgs/rn-inner-shadow-gif.gif -------------------------------------------------------------------------------- /docs/imgs/rn-inner-shadow-pressable-thumbnail.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShinMini/react-native-inner-shadow/9cb32669daf21e12b9e1684483540f08e24ebfee/docs/imgs/rn-inner-shadow-pressable-thumbnail.gif -------------------------------------------------------------------------------- /docs/imgs/rn-inner-shadow-thubnail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShinMini/react-native-inner-shadow/9cb32669daf21e12b9e1684483540f08e24ebfee/docs/imgs/rn-inner-shadow-thubnail.jpg -------------------------------------------------------------------------------- /example/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "example", 4 | "slug": "example", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "userInterfaceStyle": "light", 9 | "newArchEnabled": true, 10 | "splash": { 11 | "image": "./assets/splash-icon.png", 12 | "resizeMode": "contain", 13 | "backgroundColor": "#ffffff" 14 | }, 15 | "ios": { 16 | "supportsTablet": true, 17 | "bundleIdentifier": "innershadow.example" 18 | }, 19 | "android": { 20 | "adaptiveIcon": { 21 | "foregroundImage": "./assets/adaptive-icon.png", 22 | "backgroundColor": "#ffffff" 23 | }, 24 | "package": "innershadow.example" 25 | }, 26 | "web": { 27 | "favicon": "./assets/favicon.png" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /example/assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShinMini/react-native-inner-shadow/9cb32669daf21e12b9e1684483540f08e24ebfee/example/assets/adaptive-icon.png -------------------------------------------------------------------------------- /example/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShinMini/react-native-inner-shadow/9cb32669daf21e12b9e1684483540f08e24ebfee/example/assets/favicon.png -------------------------------------------------------------------------------- /example/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShinMini/react-native-inner-shadow/9cb32669daf21e12b9e1684483540f08e24ebfee/example/assets/icon.png -------------------------------------------------------------------------------- /example/assets/splash-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShinMini/react-native-inner-shadow/9cb32669daf21e12b9e1684483540f08e24ebfee/example/assets/splash-icon.png -------------------------------------------------------------------------------- /example/babel.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { getConfig } = require('react-native-builder-bob/babel-config'); 3 | const pkg = require('../package.json'); 4 | 5 | const root = path.resolve(__dirname, '..'); 6 | 7 | module.exports = function (api) { 8 | api.cache(true); 9 | 10 | return getConfig( 11 | { 12 | presets: ['babel-preset-expo'], 13 | }, 14 | { root, pkg } 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | import { registerRootComponent } from 'expo'; 2 | 3 | import App from './src/App'; 4 | 5 | // registerRootComponent calls AppRegistry.registerComponent('main', () => App); 6 | // It also ensures that whether you load the app in Expo Go or in a native build, 7 | // the environment is set up appropriately 8 | registerRootComponent(App); 9 | -------------------------------------------------------------------------------- /example/metro.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { getDefaultConfig } = require('@expo/metro-config'); 3 | const { getConfig } = require('react-native-builder-bob/metro-config'); 4 | const pkg = require('../package.json'); 5 | 6 | const root = path.resolve(__dirname, '..'); 7 | 8 | /** 9 | * Metro configuration 10 | * https://facebook.github.io/metro/docs/configuration 11 | * 12 | * @type {import('metro-config').MetroConfig} 13 | */ 14 | module.exports = getConfig(getDefaultConfig(__dirname), { 15 | root, 16 | pkg, 17 | project: __dirname, 18 | }); 19 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-inner-shadow-example", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo start --android", 8 | "ios": "expo start --ios", 9 | "web": "expo start --web" 10 | }, 11 | "dependencies": { 12 | "@expo/metro-runtime": "~4.0.1", 13 | "@shopify/react-native-performance": "^4.1.2", 14 | "@shopify/react-native-skia": "1.5.0", 15 | "expo": "~52.0.27", 16 | "expo-status-bar": "~2.0.1", 17 | "react": "18.3.1", 18 | "react-dom": "18.3.1", 19 | "react-native": "0.76.6", 20 | "react-native-reanimated": "~3.16.1", 21 | "react-native-web": "~0.19.13" 22 | }, 23 | "devDependencies": { 24 | "@babel/core": "^7.20.0", 25 | "react-native-builder-bob": "^0.35.2" 26 | }, 27 | "private": true 28 | } 29 | -------------------------------------------------------------------------------- /example/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, Text, View } from 'react-native'; 3 | 4 | import { 5 | LinearShadowView, 6 | ShadowPressable, 7 | ShadowToggle, 8 | ShadowView, 9 | } from 'react-native-inner-shadow'; 10 | 11 | function App(): React.JSX.Element { 12 | const [isActive, setIsActive] = React.useState(false); 13 | const onPressToggle = () => { 14 | setIsActive((prev) => !prev); 15 | }; 16 | 17 | return ( 18 | 19 | 20 | 28 | Press Me! 29 | 30 | 31 | 32 | 33 | 40 | {isActive ? 'ON' : 'OFF'} 41 | 42 | 43 | 44 | ShadowView 45 | 46 | 55 | LinearShadowView 56 | 57 | 58 | ); 59 | } 60 | 61 | const styles = StyleSheet.create({ 62 | container: { 63 | backgroundColor: '#f0f0f0', 64 | flex: 1, 65 | justifyContent: 'center', 66 | alignItems: 'center', 67 | }, 68 | toggleContainer: { 69 | paddingVertical: 10, 70 | paddingHorizontal: 10, 71 | borderRadius: 15, 72 | backgroundColor: '#ffe6a7', 73 | marginBottom: 10, 74 | }, 75 | shadowView: { 76 | justifyContent: 'center', 77 | alignItems: 'center', 78 | // borderRadius: '30%', 79 | borderRadius: 30, 80 | borderTopStartRadius: 10, 81 | borderTopLeftRadius: 20, 82 | borderTopEndRadius: 20, 83 | 84 | borderBottomLeftRadius: 4, 85 | borderBottomEndRadius: 50, 86 | marginTop: 10, 87 | padding: 10, 88 | width: '30%', 89 | aspectRatio: 1, 90 | }, 91 | shadowToggle: { 92 | backgroundColor: '#fefae0', 93 | // backgroundColor: '#AeBa40', 94 | justifyContent: 'center', 95 | alignItems: 'center', 96 | width: 126, 97 | height: 66, 98 | borderRadius: 12, 99 | 100 | borderTopLeftRadius: 50, 101 | borderTopRightRadius: 50, 102 | borderBottomLeftRadius: 10, 103 | borderBottomRightRadius: 10, 104 | }, 105 | shadowPressable: { 106 | backgroundColor: '#0081a7', 107 | justifyContent: 'center', 108 | alignItems: 'center', 109 | width: 266, 110 | height: 116, 111 | 112 | borderRadius: 15, 113 | borderTopStartRadius: 50, 114 | borderTopLeftRadius: 50, 115 | borderTopEndRadius: 10, 116 | 117 | borderBottomLeftRadius: 10, 118 | borderBottomEndRadius: 40, 119 | }, 120 | context: { 121 | fontSize: 20, 122 | color: 'white', 123 | fontWeight: 700, 124 | }, 125 | toggleContext: { 126 | fontSize: 16, 127 | color: 'gray', 128 | fontWeight: 700, 129 | }, 130 | }); 131 | 132 | export default App; 133 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "compilerOptions": { 4 | // Avoid expo-cli auto-generating a tsconfig 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | pre-commit: 2 | parallel: true 3 | commands: 4 | lint: 5 | glob: "*.{js,ts,jsx,tsx}" 6 | run: npx eslint {staged_files} 7 | types: 8 | glob: "*.{js,ts, jsx, tsx}" 9 | run: npx tsc 10 | commit-msg: 11 | parallel: true 12 | commands: 13 | commitlint: 14 | run: npx commitlint --edit 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-inner-shadow", 3 | "source": "./src/index.ts", 4 | "version": "2.3.0", 5 | "main": "./lib/commonjs/index.js", 6 | "module": "./lib/module/index.js", 7 | "types": "./lib/typescript/commonjs/src/index.d.ts", 8 | "bugs": { 9 | "url": "https://github.com/ShinMini/react-native-inner-shadow/issues" 10 | }, 11 | "author": "ShinMini (https://github.com/ShinMini)", 12 | "repository": "https://github.com/ShinMini/react-native-inner-shadow", 13 | "homepage": "https://github.com/ShinMini/react-native-inner-shadow#readme", 14 | "license": "ISC", 15 | "description": "react native inner shadows with linear gradient design UI", 16 | "scripts": { 17 | "example": "yarn workspace react-native-inner-shadow-example", 18 | "dev": "yarn example start", 19 | "start": "react-native start", 20 | "test": "npx jest src", 21 | "typecheck": "tsc", 22 | "lint": "eslint \"**/*.{js,ts,tsx}\"", 23 | "clean": "del-cli lib", 24 | "prepare": "bob build", 25 | "release": "release-it" 26 | }, 27 | "peerDependencies": { 28 | "@shopify/react-native-skia": "^2.0.0-next.2", 29 | "react": ">=18.3.1", 30 | "react-dom": ">=18.3.1", 31 | "react-native": ">=0.76.6", 32 | "react-native-reanimated": ">=3.16.1" 33 | }, 34 | "publishConfig": { 35 | "registry": "https://registry.npmjs.org/" 36 | }, 37 | "workspaces": [ 38 | "example" 39 | ], 40 | "devDependencies": { 41 | "@babel/core": "^7.20.0", 42 | "@babel/preset-env": "^7.20.0", 43 | "@babel/runtime": "^7.20.0", 44 | "@commitlint/config-conventional": "^17.0.2", 45 | "@evilmartians/lefthook": "^1.5.0", 46 | "@react-native/babel-preset": "0.75.4", 47 | "@react-native/eslint-config": "0.75.4", 48 | "@react-native/metro-config": "0.75.4", 49 | "@react-native/typescript-config": "0.75.4", 50 | "@release-it/conventional-changelog": "^9.0.2", 51 | "@types/react": "^18.2.6", 52 | "@types/react-test-renderer": "^18.0.0", 53 | "babel-jest": "^29.6.3", 54 | "commitlint": "^17.0.2", 55 | "del-cli": "^5.1.0", 56 | "eslint": "^8.19.0", 57 | "eslint-config-prettier": "^9.0.0", 58 | "eslint-plugin-prettier": "^5.2.3", 59 | "expo": "^52.0.27", 60 | "jest": "^29.6.3", 61 | "prettier": "^3.4.2", 62 | "@shopify/react-native-skia": "^2.0.0-next.2", 63 | "react": "19.0.0", 64 | "react-native": "0.78.2", 65 | "react-native-reanimated": "^3.17.2", 66 | "react-native-builder-bob": "^0.35.2", 67 | "react-test-renderer": "18.3.1", 68 | "release-it": "^17.10.0", 69 | "typescript": "5.0.4" 70 | }, 71 | "engines": { 72 | "node": ">=18" 73 | }, 74 | "packageManager": "yarn@3.6.4", 75 | "exports": { 76 | ".": { 77 | "import": { 78 | "types": "./lib/typescript/module/src/index.d.ts", 79 | "default": "./lib/module/index.js" 80 | }, 81 | "require": { 82 | "types": "./lib/typescript/commonjs/src/index.d.ts", 83 | "default": "./lib/commonjs/index.js" 84 | } 85 | } 86 | }, 87 | "files": [ 88 | "src", 89 | "lib", 90 | "*.podspec", 91 | "!**/__tests__", 92 | "babel.config.js" 93 | ], 94 | "react-native-builder-bob": { 95 | "source": "src", 96 | "output": "lib", 97 | "targets": [ 98 | [ 99 | "commonjs", 100 | { 101 | "esm": true 102 | } 103 | ], 104 | [ 105 | "module", 106 | { 107 | "esm": true 108 | } 109 | ], 110 | [ 111 | "typescript", 112 | { 113 | "project": "tsconfig.build.json", 114 | "esm": true 115 | } 116 | ] 117 | ] 118 | }, 119 | "commitlint": { 120 | "extends": [ 121 | "@commitlint/config-conventional" 122 | ] 123 | }, 124 | "release-it": { 125 | "git": { 126 | "commitMessage": "chore: release ${version}", 127 | "tagName": "v${version}" 128 | }, 129 | "npm": { 130 | "publish": true 131 | }, 132 | "github": { 133 | "release": true 134 | }, 135 | "plugins": { 136 | "@release-it/conventional-changelog": { 137 | "preset": { 138 | "name": "angular", 139 | "types": [ 140 | { 141 | "type": "feat", 142 | "section": "Features" 143 | }, 144 | { 145 | "type": "fix", 146 | "section": "Bug Fixes" 147 | }, 148 | {} 149 | ] 150 | }, 151 | "infile": "CHANGELOG.md" 152 | } 153 | } 154 | }, 155 | "prettier": { 156 | "quoteProps": "consistent", 157 | "singleQuote": true, 158 | "tabWidth": 2, 159 | "trailingComma": "es5", 160 | "useTabs": false 161 | }, 162 | "eslintConfig": { 163 | "root": true, 164 | "extends": [ 165 | "@react-native", 166 | "prettier" 167 | ], 168 | "plugins": [ 169 | "prettier" 170 | ], 171 | "rules": { 172 | "react/react-in-jsx-scope": "off", 173 | "prettier/prettier": [ 174 | "error", 175 | { 176 | "quoteProps": "consistent", 177 | "singleQuote": true, 178 | "tabWidth": 2, 179 | "trailingComma": "es5", 180 | "useTabs": false 181 | } 182 | ] 183 | } 184 | }, 185 | "eslintIgnore": [ 186 | "node_modules/", 187 | "lib/" 188 | ], 189 | "keywords": [ 190 | "react-native", 191 | "react-native-inner-shadow", 192 | "android", 193 | "ios", 194 | "react-native-reanimated", 195 | "react-native-skia", 196 | "react-native-linear-gradient", 197 | "react-native-linear-shadow", 198 | "react-native-linear-inner-shadow", 199 | "inner-shadow", 200 | "inset", 201 | "inset-shadow", 202 | "react-native-shadow", 203 | "shadow-pressable", 204 | "shadow-toggle", 205 | "inner-shadow-pressable", 206 | "inner-shadow-toggle", 207 | "shadow", 208 | "shadows", 209 | "gradient", 210 | "linear-gradient", 211 | "linear-shadow", 212 | "linear-inner-shadow" 213 | ] 214 | } 215 | -------------------------------------------------------------------------------- /src/components/CornerRadii.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { RoundedRect, type InputRRect } from '@shopify/react-native-skia'; 3 | 4 | import type { ViewStyle } from 'react-native'; 5 | import { getBorderRadius } from '../utils'; 6 | import type { SharedValue } from 'react-native-reanimated'; 7 | import { CANVAS_PADDING } from '../constants'; 8 | 9 | type CornerRadiiProps = { 10 | width: number; 11 | height: number; 12 | style?: ViewStyle; 13 | backgroundColor: SharedValue | string; 14 | children?: React.ReactNode; 15 | }; 16 | export const CornerRadii = React.memo(function CornerRadii({ 17 | width, 18 | height, 19 | style, 20 | children, 21 | backgroundColor, 22 | }: CornerRadiiProps) { 23 | const rrct = React.useMemo(() => { 24 | const { 25 | topLeftRadius, 26 | topRightRadius, 27 | bottomRightRadius, 28 | bottomLeftRadius, 29 | } = getBorderRadius({ 30 | borderRadius: style?.borderRadius, 31 | borderTopStartRadius: style?.borderTopStartRadius, 32 | borderTopLeftRadius: style?.borderTopLeftRadius, 33 | borderTopEndRadius: style?.borderTopEndRadius, 34 | borderTopRightRadius: style?.borderTopRightRadius, 35 | borderBottomStartRadius: style?.borderBottomStartRadius, 36 | borderBottomLeftRadius: style?.borderBottomLeftRadius, 37 | borderBottomEndRadius: style?.borderBottomEndRadius, 38 | borderBottomRightRadius: style?.borderBottomRightRadius, 39 | }); 40 | 41 | return { 42 | rect: { 43 | x: CANVAS_PADDING, 44 | y: CANVAS_PADDING, 45 | width: width, 46 | height: height, 47 | }, 48 | topLeft: { x: topLeftRadius, y: topLeftRadius }, 49 | topRight: { x: topRightRadius, y: topRightRadius }, 50 | bottomRight: { x: bottomRightRadius, y: bottomRightRadius }, 51 | bottomLeft: { x: bottomLeftRadius, y: bottomLeftRadius }, 52 | } satisfies InputRRect; 53 | }, [ 54 | width, 55 | height, 56 | style?.borderRadius, 57 | style?.borderTopStartRadius, 58 | style?.borderTopLeftRadius, 59 | style?.borderTopEndRadius, 60 | style?.borderTopRightRadius, 61 | style?.borderBottomStartRadius, 62 | style?.borderBottomLeftRadius, 63 | style?.borderBottomEndRadius, 64 | style?.borderBottomRightRadius, 65 | ]); 66 | 67 | return ( 68 | 69 | {children} 70 | 71 | ); 72 | }); 73 | -------------------------------------------------------------------------------- /src/components/ShadowLinearGradientFill.tsx: -------------------------------------------------------------------------------- 1 | import { LinearGradient } from '@shopify/react-native-skia'; 2 | import type { LinearInnerShadowProps } from '../types'; 3 | import { getLinearDirection } from '../utils'; 4 | import React from 'react'; 5 | 6 | /** 7 | * Internal helper component that draws the linear gradient. 8 | * You can rename this to "LinearGradientFill" or similar if you prefer. 9 | */ 10 | export default function LinearGradientFill({ 11 | width = 0, 12 | height = 0, 13 | from = 'top', 14 | to = 'bottom', 15 | colors, 16 | }: LinearInnerShadowProps) { 17 | const { start, end } = React.useMemo( 18 | () => getLinearDirection({ width, height, from, to }), 19 | [width, height, from, to] 20 | ); 21 | 22 | return ; 23 | } 24 | -------------------------------------------------------------------------------- /src/components/ShadowPressable.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import { Pressable, View } from 'react-native'; 3 | import { Canvas, Shadow } from '@shopify/react-native-skia'; 4 | import Animated from 'react-native-reanimated'; 5 | 6 | import type { 7 | LinearShadowPressableProps, 8 | ShadowPressableProps, 9 | } from '../types'; 10 | import { 11 | CANVAS_PADDING, 12 | COMMON_STYLES, 13 | DAMPING_DURATION, 14 | DAMPING_RATIO, 15 | IS_REFLECTED_LIGHT_ENABLED, 16 | } from '../constants'; 17 | 18 | import { isLinearProps } from '../utils'; 19 | 20 | import LinearGradientFill from './ShadowLinearGradientFill'; 21 | import { CornerRadii } from './CornerRadii'; 22 | 23 | import { useAnimatedOffset } from '../hooks/useAnimatedOffset'; 24 | import { useShadowProperties } from '../hooks/useShadowProperties'; 25 | 26 | const PressButton = Animated.createAnimatedComponent(Pressable); 27 | 28 | export const UnifiedShadowPressable = memo(function ShadowPressable({ 29 | width: propWidth, 30 | height: propHeight, 31 | duration = DAMPING_DURATION, 32 | damping = DAMPING_RATIO, 33 | isReflectedLightEnabled = IS_REFLECTED_LIGHT_ENABLED, 34 | style, 35 | children, 36 | onLayout: propsOnLayout, 37 | ...props 38 | }: ShadowPressableProps | LinearShadowPressableProps) { 39 | const { flatStyle, bgColor, shadowProps, layout, canRenderCanvas, onLayout } = 40 | useShadowProperties({ 41 | propWidth, 42 | propHeight, 43 | style, 44 | propsOnLayout, 45 | ...props, 46 | }); 47 | 48 | const { 49 | onPressIn, 50 | onPressOut, 51 | offset, 52 | reflectedLightOffset, 53 | inset, 54 | blurRadius, 55 | PressedAnimatedStyle, 56 | } = useAnimatedOffset({ 57 | offset: shadowProps.shadowOffset, 58 | reflectedLightOffset: shadowProps.reflectedLightOffset, 59 | blurRadius: shadowProps.shadowBlur, 60 | damping, 61 | duration, 62 | onPressIn: props.onPressIn, 63 | onPressOut: props.onPressOut, 64 | }); 65 | 66 | const isLinear = isLinearProps(props); 67 | 68 | return ( 69 | 70 | {canRenderCanvas ? ( 71 | 80 | 86 | {isLinear ? ( 87 | 92 | ) : null} 93 | 94 | 101 | 102 | {isReflectedLightEnabled ? ( 103 | 110 | ) : null} 111 | 112 | 113 | ) : null} 114 | 120 | {children} 121 | 122 | 123 | ); 124 | }); 125 | 126 | /** 127 | * ShadowPressable 128 | * ---------------- 129 | * A pressable component that casts a shadow when pressed. 130 | * The shadow effect is created using the `@shopify/react-native-skia` library. 131 | * 132 | * @remarks 133 | * See {@link ShadowPressableProps} for a linear gradient shadow. 134 | * 135 | * @example 136 | * ```ts 137 | * 138 | * Press Me! 139 | * 140 | * ``` 141 | * 142 | * @param duration - The duration of the shadow animation 143 | * @param damping - The damping factor of the shadow animation 144 | * @param isReflectedLightEnabled - Whether the reflected light effect is enabled 145 | * @param initialDepth - deprecated: set shadow depth using `shadowOffset` instead 146 | * @param shadowSpace - deprecated: set shadow depth using `shadowOffset` instead 147 | */ 148 | export const ShadowPressable: React.FC = 149 | UnifiedShadowPressable; 150 | 151 | /** 152 | * LinearShadowPressable 153 | * ---------------- 154 | * A pressable component that casts a linear gradient shadow when pressed. 155 | * The shadow effect is created using the `@shopify/react-native-skia` library. 156 | * 157 | * @remarks 158 | * See {@link LinearShadowPressableProps} for a linear gradient shadow. 159 | * 160 | * @example 161 | * ```ts 162 | * 163 | * Press Me! 164 | * 165 | * ``` 166 | * 167 | * @param duration - The duration of the shadow animation 168 | * @param damping - The damping factor of the shadow animation 169 | * @param isReflectedLightEnabled - Whether the reflected light effect is enabled 170 | * @param from - The direction of the linear gradient 171 | * @param to - The direction of the linear gradient 172 | */ 173 | export const LinearShadowPressable: React.FC = 174 | UnifiedShadowPressable; 175 | -------------------------------------------------------------------------------- /src/components/ShadowToggle.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import { Pressable, View } from 'react-native'; 3 | import { Canvas, Shadow } from '@shopify/react-native-skia'; 4 | import Animated, { 5 | interpolateColor, 6 | useAnimatedReaction, 7 | useDerivedValue, 8 | withTiming, 9 | } from 'react-native-reanimated'; 10 | 11 | import type { LinearShadowToggleProps, ShadowToggleProps } from '../types'; 12 | import { 13 | CANVAS_PADDING, 14 | COMMON_STYLES, 15 | DAMPING_DURATION, 16 | DAMPING_RATIO, 17 | INITIAL_DEPTH, 18 | IS_REFLECTED_LIGHT_ENABLED, 19 | } from '../constants'; 20 | 21 | import { isLinearProps } from '../utils'; 22 | import LinearGradientFill from './ShadowLinearGradientFill'; 23 | import { useAnimatedOffset } from '../hooks/useAnimatedOffset'; 24 | import { CornerRadii } from './CornerRadii'; 25 | import { useShadowProperties } from '../hooks/useShadowProperties'; 26 | 27 | const PressButton = Animated.createAnimatedComponent(Pressable); 28 | 29 | export const UnifiedShadowToggle = memo(function ShadowToggle({ 30 | width: propWidth, 31 | height: propHeight, 32 | isActive = false, 33 | activeColor, 34 | duration = DAMPING_DURATION, 35 | damping = DAMPING_RATIO, 36 | isReflectedLightEnabled = IS_REFLECTED_LIGHT_ENABLED, 37 | style, 38 | children, 39 | onLayout: propsOnLayout, 40 | ...props 41 | }: ShadowToggleProps | LinearShadowToggleProps) { 42 | const { flatStyle, bgColor, shadowProps, layout, canRenderCanvas, onLayout } = 43 | useShadowProperties({ 44 | propWidth, 45 | propHeight, 46 | style, 47 | propsOnLayout, 48 | ...props, 49 | }); 50 | 51 | const isLinear = isLinearProps(props); 52 | 53 | const { 54 | depth, 55 | inset, 56 | offset, 57 | reflectedLightOffset, 58 | blurRadius, 59 | PressedAnimatedStyle, 60 | } = useAnimatedOffset({ 61 | offset: shadowProps.shadowOffset, 62 | reflectedLightOffset: shadowProps.reflectedLightOffset, 63 | blurRadius: shadowProps.shadowBlur, 64 | damping, 65 | duration, 66 | onPressIn: props.onPressIn, 67 | onPressOut: props.onPressOut, 68 | }); 69 | 70 | const animatedBackgroundColor = useDerivedValue(() => 71 | interpolateColor( 72 | depth.value, 73 | [INITIAL_DEPTH, -INITIAL_DEPTH * damping], 74 | [bgColor, activeColor || bgColor] 75 | ) 76 | ); 77 | 78 | useAnimatedReaction( 79 | () => isActive, 80 | (next) => { 81 | depth.value = withTiming( 82 | next ? -INITIAL_DEPTH * damping : INITIAL_DEPTH, 83 | { 84 | duration, 85 | } 86 | ); 87 | }, 88 | [isActive] 89 | ); 90 | 91 | return ( 92 | 93 | {canRenderCanvas ? ( 94 | 103 | 109 | {isLinear ? ( 110 | 115 | ) : null} 116 | 117 | 124 | 125 | {isReflectedLightEnabled ? ( 126 | 133 | ) : null} 134 | 135 | 136 | ) : null} 137 | 142 | {children} 143 | 144 | 145 | ); 146 | }); 147 | 148 | /** 149 | * ShadowToggle 150 | * ---------------- 151 | * A toggle component that casts a shadow when active. 152 | * The shadow effect is created using the `@shopify/react-native-skia` library. 153 | * 154 | * @param isActive - Whether the shadow is active 155 | * @param activeColor - The color of the shadow when active 156 | */ 157 | export const ShadowToggle: React.FC = UnifiedShadowToggle; 158 | 159 | /** 160 | * LinearShadowToggle 161 | * ---------------- 162 | * A toggle component that casts a linear gradient shadow when active. 163 | * The shadow effect is created using the `@shopify/react-native-skia` library. 164 | * 165 | * @param isActive - Whether the shadow is active 166 | * @param activeColor - The color of the shadow when active 167 | * @param colors - The colors of the linear gradient 168 | * @param from - The direction of the linear gradient 169 | * @param to - The direction of the linear gradient 170 | */ 171 | export const LinearShadowToggle: React.FC = 172 | UnifiedShadowToggle; 173 | -------------------------------------------------------------------------------- /src/components/ShadowView.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import { View } from 'react-native'; 3 | 4 | import { isLinearProps } from '../utils'; 5 | import type { InnerShadowProps, LinearInnerShadowProps } from '../types'; 6 | import { 7 | CANVAS_PADDING, 8 | COMMON_STYLES, 9 | IS_REFLECTED_LIGHT_ENABLED, 10 | } from '../constants'; 11 | 12 | import { Canvas, Shadow } from '@shopify/react-native-skia'; 13 | import LinearGradientFill from './ShadowLinearGradientFill'; 14 | import { CornerRadii } from './CornerRadii'; 15 | 16 | import { useShadowProperties } from './../hooks/useShadowProperties'; 17 | 18 | /** 19 | * A unified interface for both "solid" (InnerShadow) and "linear" (LinearShadow). 20 | * We automatically detect "linear mode" by checking if the user provides 21 | * gradient props (colors, from, to, etc.). 22 | */ 23 | const UnifiedShadowView = memo(function UnifiedShadowView({ 24 | width: propWidth, 25 | height: propHeight, 26 | inset, 27 | isReflectedLightEnabled = IS_REFLECTED_LIGHT_ENABLED, 28 | style, 29 | onLayout: propsOnLayout, 30 | children, 31 | ...props 32 | }: InnerShadowProps | LinearInnerShadowProps) { 33 | // Extract base fields 34 | const { flatStyle, bgColor, shadowProps, layout, canRenderCanvas, onLayout } = 35 | useShadowProperties({ 36 | propWidth, 37 | propHeight, 38 | style, 39 | inset, 40 | propsOnLayout, 41 | ...props, 42 | }); 43 | // If isReflectedLightEnabled is undefined, default to `props.inset` (typical). 44 | const isLinear = isLinearProps(props); 45 | 46 | return ( 47 | 51 | {canRenderCanvas ? ( 52 | 61 | 67 | {/* Separate linear gradient */} 68 | {isLinear ? ( 69 | 74 | ) : null} 75 | 82 | {isReflectedLightEnabled ? ( 83 | 90 | ) : null} 91 | 92 | 93 | ) : null} 94 | 95 | {children} 96 | 97 | 98 | ); 99 | }); 100 | 101 | /** 102 | * ShadowView: for a basic “solid” background shadow(no gradient props). 103 | * 104 | * @remarks 105 | * See {@link InnerShadowProps} for a linear gradient background shadow. 106 | * 107 | * @example 108 | * ```ts 109 | * 110 | * ShadowView 111 | * 112 | * ``` 113 | */ 114 | export const ShadowView: React.FC = UnifiedShadowView; 115 | 116 | /** 117 | * LinearShadowView: for a linear gradient background shadow 118 | * (requires e.g. colors, from, to). 119 | * 120 | * @remarks 121 | * See {@link LinearInnerShadowProps} for a solid background shadow. 122 | * 123 | * @example 124 | * ```ts 125 | * 130 | * LinearShadowView 131 | * 132 | * ``` 133 | */ 134 | export const LinearShadowView: React.FC = 135 | UnifiedShadowView; 136 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | const CANVAS_PADDING = 50 as const; 4 | 5 | const BACKGROUND_COLOR = '#FFFFFF' as const; 6 | 7 | // These two scales are opposite each other to create a "reflected light" effect. 8 | const SHADOW_OFFSET_SCALE = 2.5 as const; 9 | const REFLECTED_LIGHT_OFFSET_SCALE = 2 as const; 10 | 11 | const INITIAL_DEPTH = 2 as const; 12 | 13 | const SHADOW_OPACITY = 0.3 as const; 14 | const SHADOW_RADIUS = 3 as const; 15 | const SHADOW_BLUR = 2 as const; 16 | const SHADOW_ELEVATION = 3 as const; 17 | 18 | const REFLECTED_LIGHT_BLUR = 3 as const; 19 | 20 | const SHADOW_COLOR = '#2F2F2FBC' as const; 21 | const REFLECTED_LIGHT_COLOR = '#FFFFFF4D' as const; 22 | 23 | const DAMPING_DURATION = 150 as const; 24 | const DAMPING_RATIO = 0.8 as const; 25 | 26 | const IS_REFLECTED_LIGHT_ENABLED = true as const; 27 | 28 | const COMMON_STYLES = StyleSheet.create({ 29 | canvasContainer: { 30 | backgroundColor: 'transparent', 31 | }, 32 | canvasWrapper: { 33 | backgroundColor: 'transparent', 34 | }, 35 | canvas: { 36 | position: 'absolute', 37 | left: -CANVAS_PADDING, 38 | top: -CANVAS_PADDING, 39 | backgroundColor: 'transparent', 40 | }, 41 | } as const); 42 | 43 | export { 44 | CANVAS_PADDING, 45 | BACKGROUND_COLOR, 46 | INITIAL_DEPTH, 47 | SHADOW_OPACITY, 48 | SHADOW_RADIUS, 49 | SHADOW_BLUR, 50 | REFLECTED_LIGHT_BLUR, 51 | SHADOW_COLOR, 52 | REFLECTED_LIGHT_COLOR, 53 | DAMPING_DURATION, 54 | DAMPING_RATIO, 55 | IS_REFLECTED_LIGHT_ENABLED, 56 | SHADOW_OFFSET_SCALE, 57 | REFLECTED_LIGHT_OFFSET_SCALE, 58 | SHADOW_ELEVATION, 59 | COMMON_STYLES, 60 | }; 61 | -------------------------------------------------------------------------------- /src/hooks/useAnimatedOffset.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | useDerivedValue, 3 | interpolate, 4 | useSharedValue, 5 | withTiming, 6 | useAnimatedStyle, 7 | } from 'react-native-reanimated'; 8 | import { INITIAL_DEPTH } from '../constants'; 9 | import type { 10 | GestureResponderEvent, 11 | PressableProps, 12 | ViewStyle, 13 | } from 'react-native'; 14 | 15 | type UseAnimatedOffsetProps = { 16 | offset: { width: number; height: number }; 17 | reflectedLightOffset: { width: number; height: number }; 18 | blurRadius: number; 19 | damping: number; 20 | duration: number; 21 | onPressIn?: PressableProps['onPressIn']; 22 | onPressOut?: PressableProps['onPressOut']; 23 | }; 24 | 25 | export function useAnimatedOffset(props: UseAnimatedOffsetProps) { 26 | const depth = useSharedValue(INITIAL_DEPTH); 27 | const inset = useDerivedValue(() => depth.value <= 0); 28 | const blurRadius = useDerivedValue(() => 29 | interpolate( 30 | depth.value, 31 | [-INITIAL_DEPTH, 0, INITIAL_DEPTH], 32 | [props.blurRadius * props.damping, 0, props.blurRadius] 33 | ) 34 | ); 35 | 36 | const onPressIn = (event: GestureResponderEvent) => { 37 | depth.value = withTiming(-INITIAL_DEPTH, { duration: props.duration }); 38 | props?.onPressIn?.(event); 39 | }; 40 | 41 | const onPressOut = (event: GestureResponderEvent) => { 42 | depth.value = withTiming(INITIAL_DEPTH, { duration: props.duration }); 43 | props?.onPressOut?.(event); 44 | }; 45 | 46 | const offset = { 47 | dx: useDerivedValue(() => 48 | interpolate( 49 | depth.value, 50 | [-INITIAL_DEPTH, 0, INITIAL_DEPTH], 51 | [props.offset.width * props.damping, 0, props.offset.width] 52 | ) 53 | ), 54 | dy: useDerivedValue(() => 55 | interpolate( 56 | depth.value, 57 | [-INITIAL_DEPTH, 0, INITIAL_DEPTH], 58 | [props.offset.height * props.damping, 0, props.offset.height] 59 | ) 60 | ), 61 | }; 62 | 63 | const reflectedLightOffset = { 64 | dx: useDerivedValue(() => 65 | interpolate( 66 | depth.value, 67 | [-INITIAL_DEPTH, 0, INITIAL_DEPTH], 68 | [ 69 | -props.reflectedLightOffset.width * props.damping, 70 | 0, 71 | props.reflectedLightOffset.width, 72 | ] 73 | ) 74 | ), 75 | dy: useDerivedValue(() => 76 | interpolate( 77 | depth.value, 78 | [-INITIAL_DEPTH, 0, INITIAL_DEPTH], 79 | [ 80 | -props.reflectedLightOffset.height * props.damping, 81 | 0, 82 | props.reflectedLightOffset.height, 83 | ] 84 | ) 85 | ), 86 | }; 87 | 88 | const translateX = useDerivedValue(() => 89 | interpolate( 90 | depth.value, 91 | [-INITIAL_DEPTH, INITIAL_DEPTH], 92 | [-props.offset.width * 0.5, 0] 93 | ) 94 | ); 95 | 96 | const translateY = useDerivedValue(() => 97 | interpolate( 98 | depth.value, 99 | [-INITIAL_DEPTH, INITIAL_DEPTH], 100 | [-props.offset.height * 0.5, 0] 101 | ) 102 | ); 103 | 104 | const PressedAnimatedStyle = useAnimatedStyle(() => { 105 | return { 106 | transform: [ 107 | { translateX: translateX.value }, 108 | { translateY: translateY.value }, 109 | ], 110 | }; 111 | }); 112 | 113 | return { 114 | onPressIn, 115 | onPressOut, 116 | depth, 117 | offset, 118 | reflectedLightOffset, 119 | inset, 120 | blurRadius, 121 | PressedAnimatedStyle, 122 | }; 123 | } 124 | -------------------------------------------------------------------------------- /src/hooks/useShadowProperties.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo, useState } from 'react'; 2 | import { 3 | StyleSheet, 4 | type LayoutChangeEvent, 5 | type ViewStyle, 6 | } from 'react-native'; 7 | import { 8 | getBackgroundColor, 9 | computeShadowProperties, 10 | isLinearProps, 11 | numerify, 12 | } from '../utils'; 13 | import type { ShadowProps, GradientLinearProps } from '../types'; 14 | import { 15 | SHADOW_BLUR, 16 | REFLECTED_LIGHT_BLUR, 17 | REFLECTED_LIGHT_COLOR, 18 | SHADOW_COLOR, 19 | } from '../constants'; 20 | 21 | interface UseShadowPropertiesParams extends ShadowProps { 22 | propWidth?: number; 23 | propHeight?: number; 24 | style?: ViewStyle; 25 | backgroundColor?: string; 26 | propsOnLayout?: (e: LayoutChangeEvent) => void; 27 | } 28 | 29 | interface ShadowPropertiesResult { 30 | flatStyle?: ViewStyle; 31 | bgColor: string; 32 | shadowProps: ReturnType; 33 | isLinear: boolean; 34 | layout: { width: number; height: number }; 35 | canRenderCanvas: boolean; 36 | onLayout: (e: LayoutChangeEvent) => void; 37 | } 38 | 39 | export const useShadowProperties = ({ 40 | propWidth, 41 | propHeight, 42 | style, 43 | backgroundColor, 44 | shadowOffset, 45 | shadowColor = SHADOW_COLOR, 46 | shadowBlur = SHADOW_BLUR, 47 | reflectedLightOffset, 48 | reflectedLightColor = REFLECTED_LIGHT_COLOR, 49 | reflectedLightBlur = REFLECTED_LIGHT_BLUR, 50 | propsOnLayout, 51 | ...props 52 | }: 53 | | UseShadowPropertiesParams 54 | | (UseShadowPropertiesParams & 55 | GradientLinearProps)): ShadowPropertiesResult => { 56 | // Flatten styles 57 | const flatStyle = useMemo(() => StyleSheet.flatten(style) || {}, [style]); 58 | 59 | // Get background color 60 | const bgColor = useMemo( 61 | () => 62 | getBackgroundColor({ 63 | backgroundColor, 64 | styleBackground: flatStyle.backgroundColor, 65 | }), 66 | [backgroundColor, flatStyle.backgroundColor] 67 | ); 68 | 69 | // Compute shadow properties 70 | const shadowProps = useMemo( 71 | () => 72 | computeShadowProperties({ 73 | shadowOffset, 74 | shadowColor, 75 | shadowBlur, 76 | reflectedLightOffset, 77 | reflectedLightColor, 78 | reflectedLightBlur, 79 | }), 80 | [ 81 | shadowOffset, 82 | shadowColor, 83 | shadowBlur, 84 | reflectedLightOffset, 85 | reflectedLightColor, 86 | reflectedLightBlur, 87 | ] 88 | ); 89 | 90 | // Check if linear gradient props are provided 91 | const isLinear = isLinearProps(props); 92 | 93 | // Handle layout 94 | const initialW = propWidth ?? numerify(flatStyle.width, 0); 95 | const initialH = propHeight ?? numerify(flatStyle.height, 0); 96 | const [layout, setLayout] = useState({ width: initialW, height: initialH }); 97 | 98 | // Create onLayout handler 99 | const onLayout = useMemo( 100 | () => (e: LayoutChangeEvent) => { 101 | propsOnLayout?.(e); 102 | if (initialW && initialH) return; 103 | const { width, height } = e.nativeEvent.layout; 104 | setLayout((prev) => 105 | prev.width === width && prev.height === height 106 | ? prev 107 | : { width, height } 108 | ); 109 | }, 110 | [initialW, initialH, propsOnLayout] 111 | ); 112 | 113 | // Check if canvas can be rendered 114 | const canRenderCanvas = Boolean(layout.width && layout.height); 115 | 116 | return { 117 | flatStyle, 118 | bgColor, 119 | shadowProps, 120 | isLinear, 121 | layout, 122 | canRenderCanvas, 123 | onLayout, 124 | }; 125 | }; 126 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export type { 2 | LINEAR_DIRECTION, 3 | ShadowProps, 4 | ShadowViewProps, 5 | InnerShadowProps, 6 | LinearInnerShadowProps, 7 | ShadowPressableProps, 8 | LinearShadowPressableProps, 9 | ShadowToggleProps, 10 | LinearShadowToggleProps, 11 | } from './types'; 12 | 13 | export { 14 | getBackgroundColor, 15 | computeShadowProperties, 16 | getLinearDirection, 17 | isLinearProps, 18 | } from './utils'; 19 | 20 | // hooks 21 | export { useShadowProperties } from './hooks/useShadowProperties'; 22 | export { useAnimatedOffset } from './hooks/useAnimatedOffset'; 23 | 24 | export { ShadowView, LinearShadowView } from './components/ShadowView'; 25 | export { 26 | ShadowPressable, 27 | LinearShadowPressable, 28 | } from './components/ShadowPressable'; 29 | export { ShadowToggle, LinearShadowToggle } from './components/ShadowToggle'; 30 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { AnimatedProp, Color } from '@shopify/react-native-skia'; 2 | import type { ReactNode } from 'react'; 3 | import type { PressableProps, ViewProps, ViewStyle } from 'react-native'; 4 | 5 | /** 6 | * ShadowViewProps represents the props accepted by shadow view components. 7 | * It can be either an InnerShadowProps or a LinearInnerShadowProps. 8 | * 9 | * @remarks 10 | * Use this type to ensure that your shadow view components receive all required 11 | * properties for rendering either an inset shadow or a linear gradient inner shadow. 12 | */ 13 | export type ShadowViewProps = InnerShadowProps | LinearInnerShadowProps; 14 | 15 | /** 16 | * ShadowProps defines the basic requirements for a shadow component. 17 | * 18 | * @remarks 19 | * These properties determine the appearance of the shadow, including its color, 20 | * offset, blur, and opacity. They also provide support for reflected light effects. 21 | * 22 | * @defaultValue 23 | * - inset: `false` 24 | * - shadowColor: `#2F2F2FBC` 25 | * - shadowOffset: `{ width: 2, height: 2 }` 26 | * - shadowBlur: `3` (range: `[0, 20]`) 27 | * - shadowRadius: `3` (range: `[0, 20]`) 28 | * - shadowOpacity: `0.3` (range: `[0, 1]`) 29 | * - reflectedLightColor: `#FFFFFF8D` 30 | * - reflectedLightOffset: `{ width: -2, height: -2 }` 31 | * - reflectedLightBlur: `3` (range: `[0, 20]`) 32 | * 33 | * @example 34 | * ```ts 35 | * const shadowProps: ShadowProps = { 36 | * inset: true, 37 | * shadowColor: '#000000', 38 | * shadowOffset: { width: 4, height: 4 }, 39 | * shadowBlur: 5, 40 | * shadowOpacity: 0.5, 41 | * }; 42 | * ``` 43 | */ 44 | export type ShadowProps = { 45 | /** 46 | * Whether to render the shadow as inset (inside the component). 47 | * @defaultValue `false` 48 | */ 49 | inset?: boolean; 50 | /** 51 | * The color of the shadow. 52 | * 53 | * @remarks 54 | * Can use the shadowColor prop to set the color of the shadow. 55 | * @defaultValue `#2F2F2FBC` 56 | */ 57 | shadowColor?: string; 58 | /** 59 | * The offset of the shadow. 60 | * 61 | * @remarks 62 | * How far the shadow is shifted horizontally (width) and vertically (height). 63 | * For an inset shadow, positive offsets often move the shadow "downward/rightward." 64 | * @defaultValue `{ width: 2, height: 2 }` 65 | */ 66 | shadowOffset?: { width: number; height: number }; 67 | /** 68 | * The blur radius for the main shadow. Higher values create softer, larger shadows. 69 | * When `inset` property is `false`(outer shadow), the shadow blur substitutes shadowOpacity (0 ~ 1) 70 | * 71 | * @defaultValue `3` - range: `[0, 20]` 72 | */ 73 | shadowBlur?: number; 74 | 75 | /** 76 | * The radius of the shadow. 77 | * 78 | * @remarks 79 | * This property is only used when `inset` is `false`. 80 | * @defaultValue `3` - range: `[0, 20]` 81 | */ 82 | shadowRadius?: number; 83 | 84 | /** 85 | * The opacity of the shadow for the outline shadow of the component. 86 | * This property is only used when `inset` is `false`. 87 | * 88 | * @defaultValue `0.3` - range: `[0, 1]` 89 | */ 90 | shadowOpacity?: number; 91 | /** 92 | * The box shadow of the shadow. 93 | * 94 | * @remarks 95 | * This is useful when you want to customize the `inset` shadow. 96 | * 97 | * @defaultValue `undefined` 98 | * 99 | * @example 100 | * ```ts 101 | * boxShadow: '0 0 10px rgba(0, 0, 0, 0.5)' 102 | * ``` 103 | */ 104 | boxShadow?: string; 105 | 106 | /** 107 | * Color of the reflected light highlight. 108 | * @defaultValue `#FFFFFF8D` 109 | */ 110 | reflectedLightColor?: string; 111 | 112 | /** 113 | * Offset for the reflected light highlight; typically the negative 114 | * of the main shadow offset to appear on the “opposite” side. 115 | * @defaultValue `{ width: -2, height: -2 }` 116 | */ 117 | reflectedLightOffset?: { width: number; height: number }; 118 | 119 | /** 120 | * The blur radius for the reflected light highlight. 121 | * @defaultValue `3` - range: `[0, 20]` 122 | */ 123 | reflectedLightBlur?: number; 124 | }; 125 | 126 | /** 127 | * InnerShadowProps defines the basic requirements for an inset-shadow component. 128 | * 129 | * For the **optimized performance**, it is **recommended** to set the `width`, `height` and `backgroundColor` of the shadowed component. 130 | * 131 | * @remarks 132 | * This interface extends React Native's ViewProps along with ShadowProps, and adds 133 | * properties specific to rendering an inner shadow. It is intended for components that 134 | * wish to render their children with an inset shadow effect. 135 | * 136 | * See {@link ShadowProps} for more information on the shadow properties. 137 | * 138 | * @example 139 | * ```tsx 140 | * 141 | * Hello, world! 142 | * 143 | * ``` 144 | */ 145 | export interface InnerShadowProps extends ViewProps, ShadowProps { 146 | /** 147 | * Content that will be nested within the shadowed box. 148 | */ 149 | children?: ReactNode; 150 | 151 | /** 152 | * Whether to enable reflected light (like a “highlight” on the opposite side of the shadow). 153 | * @defaultValue `true` 154 | */ 155 | isReflectedLightEnabled?: boolean; 156 | 157 | width?: number; 158 | height?: number; 159 | /** 160 | * The background color of the shadowed component. 161 | * @defaultValue `#FFFFFF` 162 | */ 163 | backgroundColor?: string; 164 | style?: ViewStyle; 165 | } 166 | 167 | /** 168 | * LINEAR_DIRECTION defines the four basic directions for 169 | * linear gradients. Additional or diagonal directions can be 170 | * implemented if needed (e.g., 'topLeft', 'bottomRight', etc.). 171 | */ 172 | export type LINEAR_DIRECTION = 'top' | 'bottom' | 'left' | 'right'; 173 | 174 | /** 175 | * GradientLinearProps define the properties for configuring a linear gradient. 176 | * 177 | * @remarks 178 | * These properties are used by linear inner shadow components to define gradient transitions. 179 | * 180 | * @example 181 | * ```tsx 182 | * const gradientProps: GradientLinearProps = { 183 | * from: 'top', 184 | * to: 'bottom', 185 | * colors: ['#FFFFFF', '#2F2F2FBC'], 186 | * }; 187 | * ``` 188 | */ 189 | export type GradientLinearProps = { 190 | /** 191 | * The start direction of the linear gradient. 192 | * @defaultValue `top` 193 | */ 194 | from?: LINEAR_DIRECTION; 195 | /** 196 | * The end direction of the linear gradient. 197 | * @defaultValue `bottom` 198 | */ 199 | to?: LINEAR_DIRECTION; 200 | 201 | /** 202 | * The colors of the linear gradient. 203 | * @defaultValue `['#FFFFFF', '#2F2F2FBC']` 204 | */ 205 | colors: AnimatedProp; 206 | }; 207 | 208 | /** 209 | * LinearInnerShadowViewProps extends InnerShadowProps 210 | * to incorporate linear gradient capabilities. 211 | * 212 | * The colors prop is an array of colors for the gradient. Using multiple colors 213 | * creates more visually interesting transitions. 214 | * 215 | * @remarks 216 | * In addition to all inner shadow properties, this type requires gradient properties 217 | * such as `from`, `to`, and `colors`, enabling developers to create complex gradient shadows. 218 | * 219 | * See {@link LinearShadowProps} and {@link ShadowProps} for more information. 220 | * 221 | * @example 222 | * ```tsx 223 | * 224 | * Hello, world! 225 | * 226 | * ``` 227 | */ 228 | export interface LinearInnerShadowProps 229 | extends InnerShadowProps, 230 | GradientLinearProps {} 231 | 232 | /** 233 | * ShadowPressableProps are used for pressable shadow components. 234 | * 235 | * @remarks 236 | * Extends React Native’s PressableProps to allow interactive shadow components. 237 | * Deprecated properties such as `shadowSpace` and `initialDepth` are provided for legacy support. (below v1.3.1) 238 | */ 239 | export type ShadowPressableProps = PressableProps & 240 | Omit & { 241 | /** 242 | * Deprecated. Use shadowOffset instead. 243 | * 244 | * @deprecated Use shadowOffset instead 245 | * @defaultValue `3` - range: `[0, 20]` 246 | * 247 | * If your shadow is too close to the edge of the box, it may be clipped. 248 | * I'd recommend a minimum of 3-5 pixels of space for most shadows. 249 | */ 250 | shadowSpace?: number; 251 | /** 252 | * Deprecated. Use shadowOffset instead. 253 | * 254 | * @deprecated Use shadowOffset instead 255 | * @defaultValue `5` - range: `[0, 20]` 256 | */ 257 | initialDepth?: number; 258 | /** 259 | * The duration of the shadow animation when pressed. 260 | * @defaultValue `150` 261 | */ 262 | duration?: number; 263 | /** 264 | * The damping ratio for the shadow animation. 265 | * @defaultValue `0.8` 266 | */ 267 | damping?: number; 268 | }; 269 | 270 | export type LinearShadowPressableProps = ShadowPressableProps & 271 | GradientLinearProps; 272 | 273 | /** 274 | * `ShadowToggleProps` provide properties for interactive toggleable shadow components. 275 | * 276 | * @remarks 277 | * Use this type when you need to indicate an active state for a shadow component. 278 | * 279 | * @Params -`isActive`, `activeColor`. 280 | * 281 | * See {@link ShadowPressableProps} for more information. 282 | * 283 | * @example 284 | * ```tsx 285 | * 286 | * Hello, world! 287 | * 288 | * ``` 289 | */ 290 | export type ShadowToggleProps = ShadowPressableProps & { 291 | /** 292 | * current state of the toggle 293 | * @defaultValue `false` 294 | */ 295 | isActive?: boolean; 296 | /** 297 | * The color of the active state. 298 | * @defaultValue same as `backgroundColor` 299 | */ 300 | activeColor?: string; 301 | }; 302 | 303 | export type LinearShadowToggleProps = ShadowToggleProps & GradientLinearProps; 304 | 305 | /** 306 | * `GetBackgroundColorProps` defines properties for getting background color. 307 | */ 308 | export type GetBackgroundColorProps = { 309 | backgroundColor?: string; 310 | styleBackground?: ViewStyle['backgroundColor']; 311 | }; 312 | 313 | export type GetBorderRadiusProps = { 314 | borderRadius?: ViewStyle['borderRadius']; 315 | borderTopStartRadius?: ViewStyle['borderTopStartRadius']; 316 | borderTopLeftRadius?: ViewStyle['borderTopLeftRadius']; 317 | borderTopEndRadius?: ViewStyle['borderTopEndRadius']; 318 | borderTopRightRadius?: ViewStyle['borderTopRightRadius']; 319 | borderBottomStartRadius?: ViewStyle['borderBottomStartRadius']; 320 | borderBottomLeftRadius?: ViewStyle['borderBottomLeftRadius']; 321 | borderBottomEndRadius?: ViewStyle['borderBottomEndRadius']; 322 | borderBottomRightRadius?: ViewStyle['borderBottomRightRadius']; 323 | }; 324 | 325 | /** 326 | * `ShadowPropertyConfig` defines properties for getting shadow property. 327 | */ 328 | export type ShadowPropertyConfig = Omit< 329 | ShadowProps, 330 | 'boxShadow' | 'shadowRadius' | 'shadowOpacity' 331 | >; 332 | 333 | /** 334 | * `ReflectedLightPositionConfig` defines properties for setting reflected light direction and scale. 335 | */ 336 | export type ReflectedLightPositionConfig = { 337 | inset?: boolean; 338 | reflectedLightScale?: number; 339 | baseShadowOffset: number; 340 | }; 341 | 342 | /** 343 | * `GetOuterShadowOffsetProps` defines properties for calculating outer shadow offset. 344 | */ 345 | export type GetOuterShadowOffsetProps = { 346 | elevation?: number; 347 | } & Omit< 348 | ShadowProps, 349 | 'reflectedLightColor' | 'reflectedLightOffset' | 'reflectedLightBlur' 350 | >; 351 | /** 352 | * `GetLinearDirectionProps` defines the required properties for computing a linear gradient. 353 | */ 354 | export type GetLinearDirectionProps = { 355 | width: number; 356 | height: number; 357 | from: LINEAR_DIRECTION; 358 | to: LINEAR_DIRECTION; 359 | }; 360 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { vec } from '@shopify/react-native-skia'; 2 | 3 | import type { 4 | GetBackgroundColorProps, 5 | GetBorderRadiusProps, 6 | GetLinearDirectionProps, 7 | GetOuterShadowOffsetProps, 8 | ShadowPropertyConfig, 9 | InnerShadowProps, 10 | LinearInnerShadowProps, 11 | ReflectedLightPositionConfig, 12 | } from './types'; 13 | 14 | import { 15 | BACKGROUND_COLOR, 16 | REFLECTED_LIGHT_BLUR, 17 | REFLECTED_LIGHT_COLOR, 18 | REFLECTED_LIGHT_OFFSET_SCALE, 19 | SHADOW_BLUR, 20 | SHADOW_COLOR, 21 | SHADOW_ELEVATION, 22 | SHADOW_OFFSET_SCALE, 23 | SHADOW_OPACITY, 24 | SHADOW_RADIUS, 25 | } from './constants'; 26 | 27 | /** 28 | * Converts a value to a number, returning a default value if the conversion fails. 29 | * 30 | * @privateRemarks 31 | * At this time(17.Feb.2025), we do not support the way to convert the string (percentage) to a number. 32 | * 33 | * @template T - The type of the default value. 34 | * 35 | * @param value - The value to convert to a number. 36 | * 37 | * @returns The converted number, or the default value if the conversion fails. 38 | */ 39 | export function numerify( 40 | value: unknown, 41 | defaultValue: T 42 | ) { 43 | const num = Number(value); // if value === null return 0 44 | return Number.isNaN(num) ? defaultValue : num; 45 | } 46 | 47 | export function getBorderRadius(style?: GetBorderRadiusProps) { 48 | const borderRadius = numerify(style?.borderRadius, null); 49 | 50 | const topStartRadius = numerify(style?.borderTopStartRadius, borderRadius); 51 | const topLeftRadius = numerify( 52 | style?.borderTopLeftRadius, 53 | topStartRadius ?? 0 54 | ); 55 | 56 | const topEndRadius = numerify(style?.borderTopEndRadius, borderRadius); 57 | const topRightRadius = numerify( 58 | style?.borderTopRightRadius, 59 | topEndRadius ?? 0 60 | ); 61 | 62 | const bottomEndRadius = numerify(style?.borderBottomEndRadius, borderRadius); 63 | const bottomRightRadius = numerify( 64 | style?.borderBottomRightRadius, 65 | bottomEndRadius ?? 0 66 | ); 67 | 68 | const bottomStartRadius = numerify( 69 | style?.borderBottomStartRadius, 70 | borderRadius 71 | ); 72 | const bottomLeftRadius = numerify( 73 | style?.borderBottomLeftRadius, 74 | bottomStartRadius ?? 0 75 | ); 76 | 77 | return { 78 | borderRadius, 79 | topLeftRadius, 80 | topRightRadius, 81 | bottomRightRadius, 82 | bottomLeftRadius, 83 | }; 84 | } 85 | 86 | /** 87 | * getBackgroundColor retrieves the final background color 88 | * from either: 89 | * 1) props.backgroundColor 90 | * 2) props.style.backgroundColor 91 | * 3) BACKGROUND_COLOR 92 | * 93 | * This ensures there is always a valid color for the component’s background. 94 | * 95 | * {@link GetBackgroundColorProps | props} - The props object containing background color settings. 96 | * 97 | * @returns The final background color for the component. 98 | */ 99 | export function getBackgroundColor({ 100 | backgroundColor, 101 | styleBackground, 102 | }: GetBackgroundColorProps) { 103 | const bgColor = backgroundColor ?? styleBackground ?? BACKGROUND_COLOR; 104 | 105 | return bgColor as string; 106 | } 107 | 108 | /** 109 | * computeShadowProperties determines the final configuration for both 110 | * the main shadow and any reflected light. It merges default values 111 | * with provided props to form a complete “shadow settings” object. 112 | * 113 | * - `shadowOffset` / `reflectedLightOffset`: how far the shadows/highlights 114 | * are shifted in x and y. 115 | * - `shadowColor` / `reflectedLightColor`: colors used for each effect. 116 | * - `shadowBlur` / `reflectedLightBlur`: blur radius for the softness/spread 117 | * of the shadow or highlight. 118 | * 119 | * {@link ShadowPropertyConfig} - The props object containing shadow-related settings. 120 | * 121 | * @returns `{ 122 | * shadowOffset, reflectedLightOffset, shadowColor, reflectedLightColor, shadowBlur, reflectedLightBlur }` 123 | */ 124 | export function computeShadowProperties({ 125 | inset, 126 | shadowOffset, 127 | shadowBlur, 128 | shadowColor, 129 | reflectedLightOffset, 130 | reflectedLightBlur, 131 | reflectedLightColor, 132 | }: ShadowPropertyConfig) { 133 | const shadowOffsetX = numerify(shadowOffset?.width, SHADOW_OFFSET_SCALE); 134 | const shadowOffsetY = numerify(shadowOffset?.height, SHADOW_OFFSET_SCALE); 135 | 136 | // By default, the reflected light offset is the inverse of the main shadow 137 | // so it appears on the opposite corner/side. 138 | // when `inset` property is `true`, the reflected light offset is opposite to the shadow offset 139 | const reflectedLightOffsetX = calculateReflectedLightPosition({ 140 | inset, 141 | reflectedLightScale: reflectedLightOffset?.width, 142 | baseShadowOffset: shadowOffsetX, 143 | }); 144 | 145 | const reflectedLightOffsetY = calculateReflectedLightPosition({ 146 | inset, 147 | reflectedLightScale: reflectedLightOffset?.height, 148 | baseShadowOffset: shadowOffsetY, 149 | }); 150 | 151 | // "Blur" here maps to how soft or large the shadow/highlight is. 152 | // The higher the number, the more diffuse the effect. 153 | const finalShadowBlur = Math.max(shadowBlur ?? SHADOW_BLUR, 0); 154 | const finalReflectedLightBlur = Math.max( 155 | reflectedLightBlur ?? REFLECTED_LIGHT_BLUR, 156 | 0 157 | ); 158 | 159 | // Fallback to the provided defaults if the user doesn't specify a color. 160 | const finalShadowColor = shadowColor ?? SHADOW_COLOR; 161 | const finalReflectedLightColor = reflectedLightColor ?? REFLECTED_LIGHT_COLOR; 162 | 163 | // Construct the final offsets as objects for clarity. 164 | const finalShadowOffset = { 165 | width: shadowOffsetX, 166 | height: shadowOffsetY, 167 | }; 168 | const finalReflectedLightOffset = { 169 | width: reflectedLightOffsetX, 170 | height: reflectedLightOffsetY, 171 | }; 172 | 173 | return { 174 | shadowOffset: finalShadowOffset, 175 | reflectedLightOffset: finalReflectedLightOffset, 176 | shadowColor: finalShadowColor, 177 | reflectedLightColor: finalReflectedLightColor, 178 | shadowBlur: finalShadowBlur, 179 | reflectedLightBlur: finalReflectedLightBlur, 180 | }; 181 | } 182 | 183 | function calculateReflectedLightPosition({ 184 | inset, 185 | reflectedLightScale, 186 | baseShadowOffset, 187 | }: ReflectedLightPositionConfig) { 188 | // When user provides a reflected light offset, use that. - which allows `0` and `null` 189 | if (reflectedLightScale !== undefined) return reflectedLightScale; 190 | 191 | // When shadow is 0, reflected light should be 0. 192 | if (baseShadowOffset === 0) return 0; 193 | 194 | // for matching reflected light offset direction based on inset 195 | const scaleFactor = (baseShadowOffset + REFLECTED_LIGHT_OFFSET_SCALE) / 2; 196 | 197 | // When inset is true, the reflected light should be opposite the shadow. 198 | return inset ? -scaleFactor : scaleFactor; 199 | } 200 | 201 | /** 202 | * `getOuterShadowOffset` calculates the outer shadow offset properties. 203 | * 204 | * {@link GetOuterShadowOffsetProps} - The props object containing outer shadow offset settings. 205 | * 206 | * @returns `{ shadowColor, shadowOffset, shadowBlur, shadowOpacity, shadowRadius, elevation, boxShadow }` 207 | */ 208 | export function getOuterShadowOffset({ 209 | inset, 210 | shadowColor, 211 | shadowOffset, 212 | shadowBlur, 213 | shadowOpacity = SHADOW_OPACITY, 214 | shadowRadius = SHADOW_RADIUS, 215 | elevation = SHADOW_ELEVATION, 216 | boxShadow, 217 | }: GetOuterShadowOffsetProps) { 218 | if (inset) return {}; 219 | 220 | return { 221 | shadowColor, 222 | shadowOffset, 223 | shadowBlur, 224 | shadowOpacity, 225 | shadowRadius, 226 | elevation, 227 | boxShadow, 228 | }; 229 | } 230 | 231 | /** 232 | * `getLinearDirection` calculates the start and end points for a linear gradient 233 | * based on the provided direction (from, to). 234 | * 235 | * - The direction is specified as a string, e.g., 'top', 'bottom', 'left', 'right'. 236 | * - The width and height are used to calculate the midpoints for each direction. 237 | * 238 | * {@link GetLinearDirectionProps} - The props object containing linear direction settings. 239 | * 240 | * @returns `{ start, end }` 241 | */ 242 | export function getLinearDirection({ 243 | width, 244 | height, 245 | from, 246 | to, 247 | }: GetLinearDirectionProps) { 248 | const top = vec(width / 2, 0); 249 | const bottom = vec(width / 2, height); 250 | 251 | const left = vec(0, height / 2); 252 | const right = vec(width, height / 2); 253 | 254 | const direction = { top, bottom, left, right }; 255 | return { start: direction[from], end: direction[to] }; 256 | } 257 | 258 | /** 259 | * `isLinearProps` checks if the provided props are for a linear gradient. 260 | * If the `colors` property is an array, we assume it's a linear gradient. 261 | * 262 | * @param props - see {@link InnerShadowProps} and {@link LinearInnerShadowProps} 263 | * 264 | * @returns `true` if the props are for a linear gradient, `false` otherwise. 265 | */ 266 | export function isLinearProps( 267 | props: InnerShadowProps | LinearInnerShadowProps 268 | ): props is LinearInnerShadowProps { 269 | return 'colors' in props && Array.isArray(props.colors); 270 | } 271 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "exclude": ["example", "lib"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": ".", 4 | "paths": { 5 | "react-native-inner-shadow": ["./src/index"] 6 | }, 7 | "allowUnreachableCode": false, 8 | "allowUnusedLabels": false, 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "jsx": "react-jsx", 12 | "lib": ["ESNext"], 13 | "module": "ESNext", 14 | "moduleResolution": "Bundler", 15 | "removeComments": true, 16 | "noEmit": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "noImplicitReturns": true, 19 | "noImplicitUseStrict": false, 20 | "noStrictGenericChecks": false, 21 | "noUncheckedIndexedAccess": true, 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": true, 24 | "resolveJsonModule": true, 25 | "resolvePackageJsonImports": false, 26 | "skipLibCheck": true, 27 | "strict": true, 28 | "target": "ESNext", 29 | "verbatimModuleSyntax": true 30 | } 31 | } 32 | --------------------------------------------------------------------------------