diff --git a/_astro/colors.590c63ff.css b/_astro/colors.590c63ff.css new file mode 100644 index 0000000..b7b3b9e --- /dev/null +++ b/_astro/colors.590c63ff.css @@ -0,0 +1 @@ +*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (min-width: 640px){.container{max-width:640px}}@media (min-width: 768px){.container{max-width:768px}}@media (min-width: 1024px){.container{max-width:1024px}}@media (min-width: 1280px){.container{max-width:1280px}}@media (min-width: 1536px){.container{max-width:1536px}}.prose{color:var(--tw-prose-body);max-width:65ch}.prose :where(p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where([class~=lead]):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-lead);font-size:1.25em;line-height:1.6;margin-top:1.2em;margin-bottom:1.2em}.prose :where(a):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-links);text-decoration:underline;font-weight:500}.prose :where(strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-bold);font-weight:600}.prose :where(a strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(blockquote strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(thead th strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:decimal;margin-top:1.25em;margin-bottom:1.25em;padding-left:1.625em}.prose :where(ol[type=A]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=A s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=I]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type=I s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type="1"]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:decimal}.prose :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:disc;margin-top:1.25em;margin-bottom:1.25em;padding-left:1.625em}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{font-weight:400;color:var(--tw-prose-counters)}.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-bullets)}.prose :where(dt):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;margin-top:1.25em}.prose :where(hr):not(:where([class~=not-prose],[class~=not-prose] *)){border-color:var(--tw-prose-hr);border-top-width:1px;margin-top:3em;margin-bottom:3em}.prose :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:500;font-style:italic;color:var(--tw-prose-quotes);border-left-width:.25rem;border-left-color:var(--tw-prose-quote-borders);quotes:"“""”""‘""’";margin-top:1.6em;margin-bottom:1.6em;padding-left:1em}.prose :where(blockquote p:first-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:open-quote}.prose :where(blockquote p:last-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:close-quote}.prose :where(h1):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:800;font-size:2.25em;margin-top:0;margin-bottom:.8888889em;line-height:1.1111111}.prose :where(h1 strong):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:900;color:inherit}.prose :where(h2):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:700;font-size:1.5em;margin-top:2em;margin-bottom:1em;line-height:1.3333333}.prose :where(h2 strong):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:800;color:inherit}.prose :where(h3):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;font-size:1.25em;margin-top:1.6em;margin-bottom:.6em;line-height:1.6}.prose :where(h3 strong):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:700;color:inherit}.prose :where(h4):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;margin-top:1.5em;margin-bottom:.5em;line-height:1.5}.prose :where(h4 strong):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:700;color:inherit}.prose :where(img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(picture):not(:where([class~=not-prose],[class~=not-prose] *)){display:block;margin-top:2em;margin-bottom:2em}.prose :where(kbd):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:500;font-family:inherit;color:var(--tw-prose-kbd);box-shadow:0 0 0 1px rgb(var(--tw-prose-kbd-shadows) / 10%),0 3px 0 rgb(var(--tw-prose-kbd-shadows) / 10%);font-size:.875em;border-radius:.3125rem;padding:.1875em .375em}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-code);font-weight:600;font-size:.875em}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:"`"}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:"`"}.prose :where(a code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(h1 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(h2 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.875em}.prose :where(h3 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.9em}.prose :where(h4 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(blockquote code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(thead th code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(pre):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-pre-code);background-color:var(--tw-prose-pre-bg);overflow-x:auto;font-weight:400;font-size:.875em;line-height:1.7142857;margin-top:1.7142857em;margin-bottom:1.7142857em;border-radius:.375rem;padding:.8571429em 1.1428571em}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)){background-color:transparent;border-width:0;border-radius:0;padding:0;font-weight:inherit;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:none}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:none}.prose :where(table):not(:where([class~=not-prose],[class~=not-prose] *)){width:100%;table-layout:auto;text-align:left;margin-top:2em;margin-bottom:2em;font-size:.875em;line-height:1.7142857}.prose :where(thead):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-th-borders)}.prose :where(thead th):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;vertical-align:bottom;padding-right:.5714286em;padding-bottom:.5714286em;padding-left:.5714286em}.prose :where(tbody tr):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-td-borders)}.prose :where(tbody tr:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:0}.prose :where(tbody td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:baseline}.prose :where(tfoot):not(:where([class~=not-prose],[class~=not-prose] *)){border-top-width:1px;border-top-color:var(--tw-prose-th-borders)}.prose :where(tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:top}.prose :where(figure>*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(figcaption):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-captions);font-size:.875em;line-height:1.4285714;margin-top:.8571429em}.prose{--tw-prose-body: #374151;--tw-prose-headings: #111827;--tw-prose-lead: #4b5563;--tw-prose-links: #111827;--tw-prose-bold: #111827;--tw-prose-counters: #6b7280;--tw-prose-bullets: #d1d5db;--tw-prose-hr: #e5e7eb;--tw-prose-quotes: #111827;--tw-prose-quote-borders: #e5e7eb;--tw-prose-captions: #6b7280;--tw-prose-kbd: #111827;--tw-prose-kbd-shadows: 17 24 39;--tw-prose-code: #111827;--tw-prose-pre-code: #e5e7eb;--tw-prose-pre-bg: #1f2937;--tw-prose-th-borders: #d1d5db;--tw-prose-td-borders: #e5e7eb;--tw-prose-invert-body: #d1d5db;--tw-prose-invert-headings: #fff;--tw-prose-invert-lead: #9ca3af;--tw-prose-invert-links: #fff;--tw-prose-invert-bold: #fff;--tw-prose-invert-counters: #9ca3af;--tw-prose-invert-bullets: #4b5563;--tw-prose-invert-hr: #374151;--tw-prose-invert-quotes: #f3f4f6;--tw-prose-invert-quote-borders: #374151;--tw-prose-invert-captions: #9ca3af;--tw-prose-invert-kbd: #fff;--tw-prose-invert-kbd-shadows: 255 255 255;--tw-prose-invert-code: #fff;--tw-prose-invert-pre-code: #d1d5db;--tw-prose-invert-pre-bg: rgb(0 0 0 / 50%);--tw-prose-invert-th-borders: #4b5563;--tw-prose-invert-td-borders: #374151;font-size:1rem;line-height:1.75}.prose :where(picture>img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(video):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(li):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;margin-bottom:.5em}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-left:.375em}.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-left:.375em}.prose :where(.prose>ul>li p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(.prose>ul>li>*:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ul>li>*:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(.prose>ol>li>*:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ol>li>*:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(dl):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where(dd):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;padding-left:1.625em}.prose :where(hr+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(h2+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(h3+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(h4+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(thead th:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-left:0}.prose :where(thead th:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-right:0}.prose :where(tbody td,tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){padding:.5714286em}.prose :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-left:0}.prose :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-right:0}.prose :where(figure):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(.prose>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(.prose>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0}.static{position:static}.relative{position:relative}.sticky{position:sticky}.top-8{top:2rem}.-mx-2{margin-left:-.5rem;margin-right:-.5rem}.my-4{margin-top:1rem;margin-bottom:1rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.ml-8{margin-left:2rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-8{margin-top:2rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.grid{display:grid}.contents{display:contents}.hidden{display:none}.aspect-square{aspect-ratio:1 / 1}.h-screen{height:100vh}.w-16{width:4rem}.w-64{width:16rem}.w-full{width:100%}.max-w-prose{max-width:65ch}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.list-outside{list-style-position:outside}.list-disc{list-style-type:disc}.list-square{list-style-type:square}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.overflow-y-auto{overflow-y:auto}.rounded-full{border-radius:9999px}.rounded-md{border-radius:.375rem}.border-t{border-top-width:1px}.border-solid{border-style:solid}.border-violet-600{--tw-border-opacity: 1;border-color:rgb(124 58 237 / var(--tw-border-opacity))}.bg-\[\#0A2744\]{--tw-bg-opacity: 1;background-color:rgb(10 39 68 / var(--tw-bg-opacity))}.bg-\[\#1D4061\]{--tw-bg-opacity: 1;background-color:rgb(29 64 97 / var(--tw-bg-opacity))}.bg-\[\#292568\]{--tw-bg-opacity: 1;background-color:rgb(41 37 104 / var(--tw-bg-opacity))}.bg-\[\#5A7997\]{--tw-bg-opacity: 1;background-color:rgb(90 121 151 / var(--tw-bg-opacity))}.bg-\[\#777777\]{--tw-bg-opacity: 1;background-color:rgb(119 119 119 / var(--tw-bg-opacity))}.bg-\[\#89A1B8\]{--tw-bg-opacity: 1;background-color:rgb(137 161 184 / var(--tw-bg-opacity))}.bg-\[\#dddddd\]{--tw-bg-opacity: 1;background-color:rgb(221 221 221 / var(--tw-bg-opacity))}.bg-\[\#eeeeee\]{--tw-bg-opacity: 1;background-color:rgb(238 238 238 / var(--tw-bg-opacity))}.bg-dr-blue-1{--tw-bg-opacity: 1;background-color:rgb(10 39 68 / var(--tw-bg-opacity))}.bg-dr-blue-2{--tw-bg-opacity: 1;background-color:rgb(29 64 97 / var(--tw-bg-opacity))}.bg-dr-blue-3{--tw-bg-opacity: 1;background-color:rgb(90 121 151 / var(--tw-bg-opacity))}.bg-dr-blue-4{--tw-bg-opacity: 1;background-color:rgb(137 161 184 / var(--tw-bg-opacity))}.bg-dr-gray-5{--tw-bg-opacity: 1;background-color:rgb(119 119 119 / var(--tw-bg-opacity))}.bg-dr-gray-8{--tw-bg-opacity: 1;background-color:rgb(221 221 221 / var(--tw-bg-opacity))}.bg-dr-gray-9{--tw-bg-opacity: 1;background-color:rgb(238 238 238 / var(--tw-bg-opacity))}.bg-dr-purp-drk{--tw-bg-opacity: 1;background-color:rgb(41 37 104 / var(--tw-bg-opacity))}.bg-neutral-800{--tw-bg-opacity: 1;background-color:rgb(38 38 38 / var(--tw-bg-opacity))}.bg-sky-100{--tw-bg-opacity: 1;background-color:rgb(224 242 254 / var(--tw-bg-opacity))}.bg-sky-50{--tw-bg-opacity: 1;background-color:rgb(240 249 255 / var(--tw-bg-opacity))}.bg-sky-900{--tw-bg-opacity: 1;background-color:rgb(12 74 110 / var(--tw-bg-opacity))}.p-8{padding:2rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-4{padding-left:1rem;padding-right:1rem}.px-8{padding-left:2rem;padding-right:2rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-40{padding-top:10rem;padding-bottom:10rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-2{padding-bottom:.5rem}.pb-8{padding-bottom:2rem}.pt-16{padding-top:4rem}.pt-2{padding-top:.5rem}.pt-32{padding-top:8rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-7xl{font-size:4.5rem;line-height:1}.text-xl{font-size:1.25rem;line-height:1.75rem}.font-bold{font-weight:700}.text-blue-50{--tw-text-opacity: 1;color:rgb(239 246 255 / var(--tw-text-opacity))}.text-blue-900{--tw-text-opacity: 1;color:rgb(30 58 138 / var(--tw-text-opacity))}.text-neutral-900{--tw-text-opacity: 1;color:rgb(23 23 23 / var(--tw-text-opacity))}.text-orange-400{--tw-text-opacity: 1;color:rgb(251 146 60 / var(--tw-text-opacity))}.text-sky-200{--tw-text-opacity: 1;color:rgb(186 230 253 / var(--tw-text-opacity))}.text-sky-50{--tw-text-opacity: 1;color:rgb(240 249 255 / var(--tw-text-opacity))}.text-sky-900{--tw-text-opacity: 1;color:rgb(12 74 110 / var(--tw-text-opacity))}.text-slate-300{--tw-text-opacity: 1;color:rgb(203 213 225 / var(--tw-text-opacity))}.text-slate-900{--tw-text-opacity: 1;color:rgb(15 23 42 / var(--tw-text-opacity))}.underline{text-decoration-line:underline}.hover\:bg-sky-900:hover{--tw-bg-opacity: 1;background-color:rgb(12 74 110 / var(--tw-bg-opacity))}.hover\:text-blue-600:hover{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity))}.hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.hover\:underline:hover{text-decoration-line:underline}.prose-p\:inline :is(:where(p):not(:where([class~=not-prose],[class~=not-prose] *))){display:inline}.prose-a\:text-blue-900 :is(:where(a):not(:where([class~=not-prose],[class~=not-prose] *))){--tw-text-opacity: 1;color:rgb(30 58 138 / var(--tw-text-opacity))}.prose-a\:underline :is(:where(a):not(:where([class~=not-prose],[class~=not-prose] *))){text-decoration-line:underline}.prose-a\:hover\:text-blue-600:hover :is(:where(a):not(:where([class~=not-prose],[class~=not-prose] *))){--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity))}@media (min-width: 640px){.sm\:sticky{position:sticky}.sm\:top-0{top:0}.sm\:block{display:block}.sm\:flex{display:flex}.sm\:grid{display:grid}.sm\:hidden{display:none}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:items-center{align-items:center}.sm\:justify-between{justify-content:space-between}.sm\:gap-8{gap:2rem}.sm\:gap-x-8{-moz-column-gap:2rem;column-gap:2rem}}@media (min-width: 768px){.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (min-width: 1024px){.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}} diff --git a/assets/6b960ed9.59ee9b53.css b/assets/6b960ed9.59ee9b53.css deleted file mode 100644 index 0c7d72b..0000000 --- a/assets/6b960ed9.59ee9b53.css +++ /dev/null @@ -1 +0,0 @@ -*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji"}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::-webkit-backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (min-width: 640px){.container{max-width:640px}}@media (min-width: 768px){.container{max-width:768px}}@media (min-width: 1024px){.container{max-width:1024px}}@media (min-width: 1280px){.container{max-width:1280px}}@media (min-width: 1536px){.container{max-width:1536px}}.prose{color:var(--tw-prose-body);max-width:65ch}.prose :where([class~="lead"]):not(:where([class~="not-prose"] *)){color:var(--tw-prose-lead);font-size:1.25em;line-height:1.6;margin-top:1.2em;margin-bottom:1.2em}.prose :where(a):not(:where([class~="not-prose"] *)){color:var(--tw-prose-links);text-decoration:underline;font-weight:500}.prose :where(strong):not(:where([class~="not-prose"] *)){color:var(--tw-prose-bold);font-weight:600}.prose :where(a strong):not(:where([class~="not-prose"] *)){color:inherit}.prose :where(blockquote strong):not(:where([class~="not-prose"] *)){color:inherit}.prose :where(thead th strong):not(:where([class~="not-prose"] *)){color:inherit}.prose :where(ol):not(:where([class~="not-prose"] *)){list-style-type:decimal;margin-top:1.25em;margin-bottom:1.25em;padding-left:1.625em}.prose :where(ol[type="A"]):not(:where([class~="not-prose"] *)){list-style-type:upper-alpha}.prose :where(ol[type="a"]):not(:where([class~="not-prose"] *)){list-style-type:lower-alpha}.prose :where(ol[type="A" s]):not(:where([class~="not-prose"] *)){list-style-type:upper-alpha}.prose :where(ol[type="a" s]):not(:where([class~="not-prose"] *)){list-style-type:lower-alpha}.prose :where(ol[type="I"]):not(:where([class~="not-prose"] *)){list-style-type:upper-roman}.prose :where(ol[type="i"]):not(:where([class~="not-prose"] *)){list-style-type:lower-roman}.prose :where(ol[type="I" s]):not(:where([class~="not-prose"] *)){list-style-type:upper-roman}.prose :where(ol[type="i" s]):not(:where([class~="not-prose"] *)){list-style-type:lower-roman}.prose :where(ol[type="1"]):not(:where([class~="not-prose"] *)){list-style-type:decimal}.prose :where(ul):not(:where([class~="not-prose"] *)){list-style-type:disc;margin-top:1.25em;margin-bottom:1.25em;padding-left:1.625em}.prose :where(ol > li):not(:where([class~="not-prose"] *))::marker{font-weight:400;color:var(--tw-prose-counters)}.prose :where(ul > li):not(:where([class~="not-prose"] *))::marker{color:var(--tw-prose-bullets)}.prose :where(hr):not(:where([class~="not-prose"] *)){border-color:var(--tw-prose-hr);border-top-width:1px;margin-top:3em;margin-bottom:3em}.prose :where(blockquote):not(:where([class~="not-prose"] *)){font-weight:500;font-style:italic;color:var(--tw-prose-quotes);border-left-width:.25rem;border-left-color:var(--tw-prose-quote-borders);quotes:"\201c""\201d""\2018""\2019";margin-top:1.6em;margin-bottom:1.6em;padding-left:1em}.prose :where(blockquote p:first-of-type):not(:where([class~="not-prose"] *)):before{content:open-quote}.prose :where(blockquote p:last-of-type):not(:where([class~="not-prose"] *)):after{content:close-quote}.prose :where(h1):not(:where([class~="not-prose"] *)){color:var(--tw-prose-headings);font-weight:800;font-size:2.25em;margin-top:0;margin-bottom:.8888889em;line-height:1.1111111}.prose :where(h1 strong):not(:where([class~="not-prose"] *)){font-weight:900;color:inherit}.prose :where(h2):not(:where([class~="not-prose"] *)){color:var(--tw-prose-headings);font-weight:700;font-size:1.5em;margin-top:2em;margin-bottom:1em;line-height:1.3333333}.prose :where(h2 strong):not(:where([class~="not-prose"] *)){font-weight:800;color:inherit}.prose :where(h3):not(:where([class~="not-prose"] *)){color:var(--tw-prose-headings);font-weight:600;font-size:1.25em;margin-top:1.6em;margin-bottom:.6em;line-height:1.6}.prose :where(h3 strong):not(:where([class~="not-prose"] *)){font-weight:700;color:inherit}.prose :where(h4):not(:where([class~="not-prose"] *)){color:var(--tw-prose-headings);font-weight:600;margin-top:1.5em;margin-bottom:.5em;line-height:1.5}.prose :where(h4 strong):not(:where([class~="not-prose"] *)){font-weight:700;color:inherit}.prose :where(img):not(:where([class~="not-prose"] *)){margin-top:2em;margin-bottom:2em}.prose :where(figure > *):not(:where([class~="not-prose"] *)){margin-top:0;margin-bottom:0}.prose :where(figcaption):not(:where([class~="not-prose"] *)){color:var(--tw-prose-captions);font-size:.875em;line-height:1.4285714;margin-top:.8571429em}.prose :where(code):not(:where([class~="not-prose"] *)){color:var(--tw-prose-code);font-weight:600;font-size:.875em}.prose :where(code):not(:where([class~="not-prose"] *)):before{content:"`"}.prose :where(code):not(:where([class~="not-prose"] *)):after{content:"`"}.prose :where(a code):not(:where([class~="not-prose"] *)){color:inherit}.prose :where(h1 code):not(:where([class~="not-prose"] *)){color:inherit}.prose :where(h2 code):not(:where([class~="not-prose"] *)){color:inherit;font-size:.875em}.prose :where(h3 code):not(:where([class~="not-prose"] *)){color:inherit;font-size:.9em}.prose :where(h4 code):not(:where([class~="not-prose"] *)){color:inherit}.prose :where(blockquote code):not(:where([class~="not-prose"] *)){color:inherit}.prose :where(thead th code):not(:where([class~="not-prose"] *)){color:inherit}.prose :where(pre):not(:where([class~="not-prose"] *)){color:var(--tw-prose-pre-code);background-color:var(--tw-prose-pre-bg);overflow-x:auto;font-weight:400;font-size:.875em;line-height:1.7142857;margin-top:1.7142857em;margin-bottom:1.7142857em;border-radius:.375rem;padding:.8571429em 1.1428571em}.prose :where(pre code):not(:where([class~="not-prose"] *)){background-color:transparent;border-width:0;border-radius:0;padding:0;font-weight:inherit;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit}.prose :where(pre code):not(:where([class~="not-prose"] *)):before{content:none}.prose :where(pre code):not(:where([class~="not-prose"] *)):after{content:none}.prose :where(table):not(:where([class~="not-prose"] *)){width:100%;table-layout:auto;text-align:left;margin-top:2em;margin-bottom:2em;font-size:.875em;line-height:1.7142857}.prose :where(thead):not(:where([class~="not-prose"] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-th-borders)}.prose :where(thead th):not(:where([class~="not-prose"] *)){color:var(--tw-prose-headings);font-weight:600;vertical-align:bottom;padding-right:.5714286em;padding-bottom:.5714286em;padding-left:.5714286em}.prose :where(tbody tr):not(:where([class~="not-prose"] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-td-borders)}.prose :where(tbody tr:last-child):not(:where([class~="not-prose"] *)){border-bottom-width:0}.prose :where(tbody td):not(:where([class~="not-prose"] *)){vertical-align:baseline}.prose :where(tfoot):not(:where([class~="not-prose"] *)){border-top-width:1px;border-top-color:var(--tw-prose-th-borders)}.prose :where(tfoot td):not(:where([class~="not-prose"] *)){vertical-align:top}.prose{--tw-prose-body: #374151;--tw-prose-headings: #111827;--tw-prose-lead: #4b5563;--tw-prose-links: #111827;--tw-prose-bold: #111827;--tw-prose-counters: #6b7280;--tw-prose-bullets: #d1d5db;--tw-prose-hr: #e5e7eb;--tw-prose-quotes: #111827;--tw-prose-quote-borders: #e5e7eb;--tw-prose-captions: #6b7280;--tw-prose-code: #111827;--tw-prose-pre-code: #e5e7eb;--tw-prose-pre-bg: #1f2937;--tw-prose-th-borders: #d1d5db;--tw-prose-td-borders: #e5e7eb;--tw-prose-invert-body: #d1d5db;--tw-prose-invert-headings: #fff;--tw-prose-invert-lead: #9ca3af;--tw-prose-invert-links: #fff;--tw-prose-invert-bold: #fff;--tw-prose-invert-counters: #9ca3af;--tw-prose-invert-bullets: #4b5563;--tw-prose-invert-hr: #374151;--tw-prose-invert-quotes: #f3f4f6;--tw-prose-invert-quote-borders: #374151;--tw-prose-invert-captions: #9ca3af;--tw-prose-invert-code: #fff;--tw-prose-invert-pre-code: #d1d5db;--tw-prose-invert-pre-bg: rgb(0 0 0 / 50%);--tw-prose-invert-th-borders: #4b5563;--tw-prose-invert-td-borders: #374151;font-size:1rem;line-height:1.75}.prose :where(p):not(:where([class~="not-prose"] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where(video):not(:where([class~="not-prose"] *)){margin-top:2em;margin-bottom:2em}.prose :where(figure):not(:where([class~="not-prose"] *)){margin-top:2em;margin-bottom:2em}.prose :where(li):not(:where([class~="not-prose"] *)){margin-top:.5em;margin-bottom:.5em}.prose :where(ol > li):not(:where([class~="not-prose"] *)){padding-left:.375em}.prose :where(ul > li):not(:where([class~="not-prose"] *)){padding-left:.375em}.prose :where(.prose > ul > li p):not(:where([class~="not-prose"] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(.prose > ul > li > *:first-child):not(:where([class~="not-prose"] *)){margin-top:1.25em}.prose :where(.prose > ul > li > *:last-child):not(:where([class~="not-prose"] *)){margin-bottom:1.25em}.prose :where(.prose > ol > li > *:first-child):not(:where([class~="not-prose"] *)){margin-top:1.25em}.prose :where(.prose > ol > li > *:last-child):not(:where([class~="not-prose"] *)){margin-bottom:1.25em}.prose :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~="not-prose"] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(hr + *):not(:where([class~="not-prose"] *)){margin-top:0}.prose :where(h2 + *):not(:where([class~="not-prose"] *)){margin-top:0}.prose :where(h3 + *):not(:where([class~="not-prose"] *)){margin-top:0}.prose :where(h4 + *):not(:where([class~="not-prose"] *)){margin-top:0}.prose :where(thead th:first-child):not(:where([class~="not-prose"] *)){padding-left:0}.prose :where(thead th:last-child):not(:where([class~="not-prose"] *)){padding-right:0}.prose :where(tbody td,tfoot td):not(:where([class~="not-prose"] *)){padding:.5714286em}.prose :where(tbody td:first-child,tfoot td:first-child):not(:where([class~="not-prose"] *)){padding-left:0}.prose :where(tbody td:last-child,tfoot td:last-child):not(:where([class~="not-prose"] *)){padding-right:0}.prose :where(.prose > :first-child):not(:where([class~="not-prose"] *)){margin-top:0}.prose :where(.prose > :last-child):not(:where([class~="not-prose"] *)){margin-bottom:0}.prose-sm :where(.prose > ul > li p):not(:where([class~="not-prose"] *)){margin-top:.5714286em;margin-bottom:.5714286em}.prose-sm :where(.prose > ul > li > *:first-child):not(:where([class~="not-prose"] *)){margin-top:1.1428571em}.prose-sm :where(.prose > ul > li > *:last-child):not(:where([class~="not-prose"] *)){margin-bottom:1.1428571em}.prose-sm :where(.prose > ol > li > *:first-child):not(:where([class~="not-prose"] *)){margin-top:1.1428571em}.prose-sm :where(.prose > ol > li > *:last-child):not(:where([class~="not-prose"] *)){margin-bottom:1.1428571em}.prose-sm :where(.prose > :first-child):not(:where([class~="not-prose"] *)){margin-top:0}.prose-sm :where(.prose > :last-child):not(:where([class~="not-prose"] *)){margin-bottom:0}.prose-base :where(.prose > ul > li p):not(:where([class~="not-prose"] *)){margin-top:.75em;margin-bottom:.75em}.prose-base :where(.prose > ul > li > *:first-child):not(:where([class~="not-prose"] *)){margin-top:1.25em}.prose-base :where(.prose > ul > li > *:last-child):not(:where([class~="not-prose"] *)){margin-bottom:1.25em}.prose-base :where(.prose > ol > li > *:first-child):not(:where([class~="not-prose"] *)){margin-top:1.25em}.prose-base :where(.prose > ol > li > *:last-child):not(:where([class~="not-prose"] *)){margin-bottom:1.25em}.prose-base :where(.prose > :first-child):not(:where([class~="not-prose"] *)){margin-top:0}.prose-base :where(.prose > :last-child):not(:where([class~="not-prose"] *)){margin-bottom:0}.prose-lg :where(.prose > ul > li p):not(:where([class~="not-prose"] *)){margin-top:.8888889em;margin-bottom:.8888889em}.prose-lg :where(.prose > ul > li > *:first-child):not(:where([class~="not-prose"] *)){margin-top:1.3333333em}.prose-lg :where(.prose > ul > li > *:last-child):not(:where([class~="not-prose"] *)){margin-bottom:1.3333333em}.prose-lg :where(.prose > ol > li > *:first-child):not(:where([class~="not-prose"] *)){margin-top:1.3333333em}.prose-lg :where(.prose > ol > li > *:last-child):not(:where([class~="not-prose"] *)){margin-bottom:1.3333333em}.prose-lg :where(.prose > :first-child):not(:where([class~="not-prose"] *)){margin-top:0}.prose-lg :where(.prose > :last-child):not(:where([class~="not-prose"] *)){margin-bottom:0}.prose-xl :where(.prose > ul > li p):not(:where([class~="not-prose"] *)){margin-top:.8em;margin-bottom:.8em}.prose-xl :where(.prose > ul > li > *:first-child):not(:where([class~="not-prose"] *)){margin-top:1.2em}.prose-xl :where(.prose > ul > li > *:last-child):not(:where([class~="not-prose"] *)){margin-bottom:1.2em}.prose-xl :where(.prose > ol > li > *:first-child):not(:where([class~="not-prose"] *)){margin-top:1.2em}.prose-xl :where(.prose > ol > li > *:last-child):not(:where([class~="not-prose"] *)){margin-bottom:1.2em}.prose-xl :where(.prose > :first-child):not(:where([class~="not-prose"] *)){margin-top:0}.prose-xl :where(.prose > :last-child):not(:where([class~="not-prose"] *)){margin-bottom:0}.prose-2xl :where(.prose > ul > li p):not(:where([class~="not-prose"] *)){margin-top:.8333333em;margin-bottom:.8333333em}.prose-2xl :where(.prose > ul > li > *:first-child):not(:where([class~="not-prose"] *)){margin-top:1.3333333em}.prose-2xl :where(.prose > ul > li > *:last-child):not(:where([class~="not-prose"] *)){margin-bottom:1.3333333em}.prose-2xl :where(.prose > ol > li > *:first-child):not(:where([class~="not-prose"] *)){margin-top:1.3333333em}.prose-2xl :where(.prose > ol > li > *:last-child):not(:where([class~="not-prose"] *)){margin-bottom:1.3333333em}.prose-2xl :where(.prose > :first-child):not(:where([class~="not-prose"] *)){margin-top:0}.prose-2xl :where(.prose > :last-child):not(:where([class~="not-prose"] *)){margin-bottom:0}.static{position:static}.relative{position:relative}.sticky{position:sticky}.top-8{top:2rem}.my-4{margin-top:1rem;margin-bottom:1rem}.-mx-2{margin-left:-.5rem;margin-right:-.5rem}.mt-8{margin-top:2rem}.mt-2{margin-top:.5rem}.mb-4{margin-bottom:1rem}.mb-2{margin-bottom:.5rem}.mt-4{margin-top:1rem}.ml-8{margin-left:2rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.grid{display:grid}.contents{display:contents}.hidden{display:none}.aspect-square{aspect-ratio:1 / 1}.h-screen{height:100vh}.w-16{width:4rem}.w-64{width:16rem}.w-full{width:100%}.max-w-prose{max-width:65ch}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.list-outside{list-style-position:outside}.list-square{list-style-type:square}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.overflow-y-auto{overflow-y:auto}.rounded-md{border-radius:.375rem}.rounded-full{border-radius:9999px}.border-t{border-top-width:1px}.border-solid{border-style:solid}.border-violet-600{--tw-border-opacity: 1;border-color:rgb(124 58 237 / var(--tw-border-opacity))}.bg-sky-50{--tw-bg-opacity: 1;background-color:rgb(240 249 255 / var(--tw-bg-opacity))}.bg-sky-100{--tw-bg-opacity: 1;background-color:rgb(224 242 254 / var(--tw-bg-opacity))}.bg-neutral-800{--tw-bg-opacity: 1;background-color:rgb(38 38 38 / var(--tw-bg-opacity))}.bg-\[\#292568\]{--tw-bg-opacity: 1;background-color:rgb(41 37 104 / var(--tw-bg-opacity))}.bg-\[\#0A2744\]{--tw-bg-opacity: 1;background-color:rgb(10 39 68 / var(--tw-bg-opacity))}.bg-\[\#1D4061\]{--tw-bg-opacity: 1;background-color:rgb(29 64 97 / var(--tw-bg-opacity))}.bg-\[\#5A7997\]{--tw-bg-opacity: 1;background-color:rgb(90 121 151 / var(--tw-bg-opacity))}.bg-\[\#89A1B8\]{--tw-bg-opacity: 1;background-color:rgb(137 161 184 / var(--tw-bg-opacity))}.bg-\[\#eeeeee\]{--tw-bg-opacity: 1;background-color:rgb(238 238 238 / var(--tw-bg-opacity))}.bg-\[\#dddddd\]{--tw-bg-opacity: 1;background-color:rgb(221 221 221 / var(--tw-bg-opacity))}.bg-\[\#777777\]{--tw-bg-opacity: 1;background-color:rgb(119 119 119 / var(--tw-bg-opacity))}.bg-sky-900{--tw-bg-opacity: 1;background-color:rgb(12 74 110 / var(--tw-bg-opacity))}.p-8{padding:2rem}.py-4{padding-top:1rem;padding-bottom:1rem}.px-2{padding-left:.5rem;padding-right:.5rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.px-8{padding-left:2rem;padding-right:2rem}.px-4{padding-left:1rem;padding-right:1rem}.py-8{padding-top:2rem;padding-bottom:2rem}.py-40{padding-top:10rem;padding-bottom:10rem}.pb-2{padding-bottom:.5rem}.pt-16{padding-top:4rem}.pb-8{padding-bottom:2rem}.pt-2{padding-top:.5rem}.pt-32{padding-top:8rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-7xl{font-size:4.5rem;line-height:1}.font-bold{font-weight:700}.text-blue-900{--tw-text-opacity: 1;color:rgb(30 58 138 / var(--tw-text-opacity))}.text-slate-900{--tw-text-opacity: 1;color:rgb(15 23 42 / var(--tw-text-opacity))}.text-orange-400{--tw-text-opacity: 1;color:rgb(251 146 60 / var(--tw-text-opacity))}.text-neutral-900{--tw-text-opacity: 1;color:rgb(23 23 23 / var(--tw-text-opacity))}.text-sky-900{--tw-text-opacity: 1;color:rgb(12 74 110 / var(--tw-text-opacity))}.text-blue-50{--tw-text-opacity: 1;color:rgb(239 246 255 / var(--tw-text-opacity))}.text-sky-50{--tw-text-opacity: 1;color:rgb(240 249 255 / var(--tw-text-opacity))}.text-slate-300{--tw-text-opacity: 1;color:rgb(203 213 225 / var(--tw-text-opacity))}.text-sky-200{--tw-text-opacity: 1;color:rgb(186 230 253 / var(--tw-text-opacity))}.underline{text-decoration-line:underline}.hover\:bg-sky-900:hover{--tw-bg-opacity: 1;background-color:rgb(12 74 110 / var(--tw-bg-opacity))}.hover\:text-blue-600:hover{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity))}.hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.hover\:underline:hover{text-decoration-line:underline}.prose-h1\:font-sans :is(:where(h1):not(:where([class~="not-prose"] *))){font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji"}.prose-h2\:font-sans :is(:where(h2):not(:where([class~="not-prose"] *))){font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji"}.prose-h3\:font-sans :is(:where(h3):not(:where([class~="not-prose"] *))){font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji"}.prose-h4\:font-sans :is(:where(h4):not(:where([class~="not-prose"] *))){font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji"}.prose-h5\:font-sans :is(:where(h5):not(:where([class~="not-prose"] *))){font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji"}.prose-h6\:font-sans :is(:where(h6):not(:where([class~="not-prose"] *))){font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji"}.prose-p\:inline :is(:where(p):not(:where([class~="not-prose"] *))){display:inline}.prose-a\:text-red-600 :is(:where(a):not(:where([class~="not-prose"] *))){--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity))}.prose-a\:text-blue-900 :is(:where(a):not(:where([class~="not-prose"] *))){--tw-text-opacity: 1;color:rgb(30 58 138 / var(--tw-text-opacity))}.prose-a\:underline :is(:where(a):not(:where([class~="not-prose"] *))){text-decoration-line:underline}.prose-a\:no-underline :is(:where(a):not(:where([class~="not-prose"] *))){text-decoration-line:none}.hover\:prose-a\:text-red-400 :is(:where(a):not(:where([class~="not-prose"] *))):hover{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity))}.prose-a\:hover\:text-blue-600:hover :is(:where(a):not(:where([class~="not-prose"] *))){--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity))}.hover\:prose-a\:underline :is(:where(a):not(:where([class~="not-prose"] *))):hover{text-decoration-line:underline}.hover\:prose-a\:decoration-2 :is(:where(a):not(:where([class~="not-prose"] *))):hover{text-decoration-thickness:2px}.prose-strong\:font-sans :is(:where(strong):not(:where([class~="not-prose"] *))){font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji"}@media (min-width: 640px){.sm\:sticky{position:sticky}.sm\:top-0{top:0}.sm\:block{display:block}.sm\:flex{display:flex}.sm\:grid{display:grid}.sm\:hidden{display:none}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:items-center{align-items:center}.sm\:justify-between{justify-content:space-between}.sm\:gap-8{gap:2rem}.sm\:gap-x-8{-moz-column-gap:2rem;column-gap:2rem}}@media (min-width: 768px){.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (min-width: 1024px){.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}} diff --git a/blog/2012-08-20-sublime-text-keymapping/index.html b/blog/2012-08-20-sublime-text-keymapping/index.html deleted file mode 100644 index 6e3f484..0000000 --- a/blog/2012-08-20-sublime-text-keymapping/index.html +++ /dev/null @@ -1,71 +0,0 @@ - - -
- - - - - - - - - -Made some keymappings in Sublime Text 2.
-Open the keymap files by going to “Sublime Text 2” > “Preferences” > “Key Bindings - User”
-This is what my key bindings file looks like:
- -I opened up the Sublime Text 2 packages directories in Sublime Text 2, and search for the commands I wanted to add key bindings to. The names aren’t always obvious.
-It turns out that the first two of these commands are from the context menu (reachable by right clicking). The context menu is described in ~/Library/Application Support/Sublime Text 2/Packages/Default/Context.sublime-menu. I copy/pasted the commands from Context.sublime-menu to the keybindings file and added key binds.
-The reindent command is found in the command palette (bound to super+p by default). The commands in there are found in ~/Library/Application Support/Sublime Text 2/Packages/Default/Default.sublime-commands
-Here is a sweet pretty log command that I found somewhere in my ~/.gitconfig
.
Here’s a script to show the git branch in the command prompt. I color the git branch to quickly show thestate of the working directory. RED means there are unstaged changes. YELLOW means the working directory and index match. And GREEN means working directory is clean (nothing to commit).
-This is known to work with git version 1.7.12. However, git version 1.7.6.4 doesn’t properly support the -b
flag with the --porcelain
flags.
RED, YELLOW, GREEN, and NO_COLOUR are color settings.
-The function parse_git_branch parses the branch from the git status -b --porcelain
command. I used head
to take the first lineusing sed to run the regular expressions.
parse_git_status echo’s RED or GREEN depending if the directory is clean or not. git status --porcelain
is a minimalist git status that should be used for scripting. In the first line, I pass stderr
to stdin
so that I can grep it for “not a git repository.”
The PS1 line sets the prompt. The $(function_name) runs the function every time the prompt is accessed, which is what you want for a prompt. If it were merely $(function_name) or function_name
, the function would be run only once.
Start at http://twitter.github.com/bootstrap/javascript.html#affix
-Add a .myelement.affix {top: 50px;}
style to set where the nav should stop at.
Xcode searches all available documentation sets by default. This is annoying when developing for the latest version of iOS, and a result comes back from every previous documentation set.
-Clicking on the little magnifying glass in the search box shows a Find Options button. Clicking that shows how to remove extraneous documentation sets from the search results.
-Here is a picture from the Xcode documentation showing how:
- -Autolayout in iOS enables responsive design. It’s more enjoyable than computing frames by hand; however, it’s a bit wordy when writing views in code. And a lot of the autolayout api look the similar. I’ve been using some helper code to encapsulate some of the calls. I published them in a Cocoa pod. Here is a tour of the features of this Cocoa pod.
-There are two categories that I made. The first is a category on NSLayoutConstraint
for making raw NSLayoutConstraint
s. The second is a category on UIView
for adding subviews and layout to a view.
NSLayoutConstraint (DRAutolayout)
The design philosophy is to provide all possible NSLayoutConstraint
constraints, but use method names instead of constants to select the different ones.
For example, instead of writing out [view addConstraint:[NSLayoutConstraint constraintWithItem:view1 attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:view2 attribute:NSLayoutAttributeCenterX multiplier:1.0 constant:0.0]]
in native parlance in order to constrain view1’s center to view2’s center, I write [view addConstraint:[NSLayoutConstraint centerX:view1 toCenterX:view2]]
, which is easier for me to read.
Further, I made helper methods to constrain view tops to view tops and view rights to view rights. [NSLayoutConstraint top:view1 toTop:view2 multiplier:1.0 constant:0]
and [NSLayoutConstraint right:view1 toRight:view2 multiplier:1.0 constant:0]
, respectively. To make subviews cling to the top, sides, and button of the superview. Of course, there is left:toLeft:multiplier:constant:
also.
I also found it useful to constrain lefts to rights when placing views next to each other with some spacing between. [NSLayoutConstraint left:self.topBar toRight:self.menuButton multiplier:1.0 constant:10]
.
You can leave off the multiplier
and constant
arguments. So [NSLayoutConstraint right:view1 toRight:view2 multiplier:1.0 constant:0]
and [NSLayoutConstraint right:view1 toRight:view2]
are equivalent.
UIView (DRAutoLayout)
The second category is on UIView and is useful for making a simple flow layout down the middle of the screen, placing related objects to the right and left. The demo app uses this category. I recommend placing all subviews to a UIScrollView.
-Remember to both placeAtTop:
and placeAtBottom:
in relation to the UIScrollView
! This tells autolayout where the top and bottom of the content is for the UIScrollView
. If you don’t do this, and you can set the contentSize of the UIScrollView
manually, the UIScrollView
might not scroll as expected.
Then after calling addSubview with all your subviews, you can arrange them by calling [scrollView place:view2 below:view1 distance:20]
and [scrollView place:view2 below:view1 distance:20]
. Then [scrollView horizontallyCenterSubviews:@[view1, view2]]
to align them all in the center.
To place something to the left or right of something in the center, use the place:leftOf:distance
method: [scrollView place:leftView leftOf:centerView distance:40]
. The same works to place something to the right: [scrollView place:rightView rightOf:centerView distance:40]
.
I hope you find these autolayout helper methods useful. I really like autolayout. And I like these helper methods because they increase the clarity of the code while reducing the amount of code to read.
-Checkout out the project at https://github.com/danramteke/DRAutolayout or add pod 'DRAutolayout'
to your Podfile.
This package is no longer maintained.
-At Cyrus, we recently fielded a proposal to convert an existing app on the App Store from Objective-C to RubyMotion. I’ll give some background to the project and client, describe what we did, and give some recommendations.
-First, a little background on the proposal. The client already had a pure Objective-C app in the App Store. They already had a design firm and a consulting firm that they had a a good working relationship with. They were Rails developers, curious about how RubyMotion could benefit them.
-Also, they were pushing new features constantly. The other firm writing new features while we were translating the app to RubyMotion seemed like a recipe for continual merge conflicts. We would have been happy to implement new features for them. Stopping development to translate the app to RubyMotion didn’t seem like a good use of their time.
-Here’s what we did. We planned to vendor the Objective-C project, convert the application delegate to Ruby, and then pull over one controller at a time until we got everything in Ruby that would be likely to change. We planned to leave in Objective-C all the dropped-in third party libraries.
-We started by vendoring the entire Objective-C project. And already hit some bumps in the road. Because the other dev shop used the .pch
file (pre-compiled headers file) to require fewer #import
statements in their files, we couldn’t simply drop the entire project in a vendor sub-folder in our RubyMotion project. Further, the cocoapods wouldn’t load for the vendored project. Because of all these missing dependencies, we had to take a different route.
Instead, we built the existing project as a static framework for iOS, remembering to properly export headers. This approach didn’t require us tp fiddle with the .pch
file, muck with pre-compiler directives, or do any other shenanigans with dependencies. This gave us a .a
file that we put in the vendor directory of our RubyMotion project. (Although we found blog posts describing how to do so, we didn’t bother making a .a
file for both the simulator and the iPhone - we were spiking this out to try to get it to work.) Here is the line from our Rakefile
app.vendor_project('vendor/libclientname-static', :static, :force_load => false,
- :products => ['libclientname-static.a'],
- :headers_dir => "include/libclientname-static")
-After translating the application delegate, vendoring the appropriate included frameworks, and setting up the cocoapods, we had a stable foundation to build on. We were able to reference controllers in the storyboard, and instantiate views from the vendored .a
file.
Through this prep work, we discovered that converting a project from Objective-C to RubyMotion is doable. It’s even doable to translate one class at a time, iteratively, until the project feels right. The cost-to-benefit of stopping to translate an already existing app is very dependent on the environment the app is being developed in. When new features are important, not translating makes sense. If a team of Ruby developers is taking over an app, a translation makes more sense.
-Update! The information in this blog post is mostly irrelevent now that you can type rake crashlog:device
to symbolicate crash logs from your device as of RubyMotion 2.18. Thanks!
Symbolicating a crash log is the process of finding a line number in source from a crash log. Crash logs often look like this. This specific process is interesting in particular to RubyMotion because Apple’s Xcode doesn’t symbolicate to Ruby code.
-Now let’s see how it’s done.
-After running rake device
, you have binary files in the build directory, including a .dSYM
file. Go ahead and cd
into that directory, I’ll be typing all commands from that directory. Here is the directory listing for my build/iPhoneOS-7.0-Development
directory:
A good example to use is this crash log I received from our customer: Crash Trace Binary Images Another good resource is this handy guide to getting crash logs from Apple devices: Pocket
-Next, we’re going to use a command called atos. You’ll need the load address, the architecture, and the list of addresses from the crash log. I’ll show where to get these from the crash log.
-Here is the command I typed based on the crash log example: xcrun atos -l 0xed000 -arch armv7 0x00a22a30 0x00a22b56 0x0098caec 0x00a20866 0x00a0a930 0x00a09a66 0x00a0a420 0x0080b524 0x0081241e 0x008124ae -o Redacted\ \(Dev\).dSYM/Contents/Resources/DWARF/Redacted\ \(Dev\)
The value for the -l
flag is taken from the Binary Images section. That first number there. The value for the -arch
flag is arm7
since the binaries were built for an iOS device.
The rest of the hex numbers are taken from the first screen shot of the crash log. I copy/pasted them from the crash log for now (would like to hear of a better way). And I got this output:
-got symbolicator for Redacted (Dev).dSYM/Contents/Resources/DWARF/Redacted (Dev), base address 4000
-__vm_raise() (in Redacted (Dev)) + 276
-rb_vm_raise (in Redacted (Dev)) + 82
-rb_frozen_class_p (in Redacted (Dev)) + 72
-rb_vm_method_missing (in Redacted (Dev)) + 358
-rb_vm_dispatch (in Redacted (Dev)) + 3060
-rb_vm_trigger_method_missing (in Redacted (Dev)) + 522
-rb_vm_dispatch (in Redacted (Dev)) + 1764
-vm_fast_plus (in Redacted (Dev)) + 356
-__unnamed_28 (in Redacted (Dev)) + 274
-rb_scope__commandDidStart:__ (in Redacted (Dev)) (login_view_controller.rb:56)
-As you can see, the crash was in the login controller. Looking at that file, it’s the Facebook log in code.
-Although having to copy/paste addresses into the command line isn’t ideal, the main goal is retrieving a line number in ruby code (although off by one) from an otherwise opaque crash log intended for Xcode. And this allows further debugging.
-I was working on a project for a client where I was wrapping their website into a WKWebView
in order to provide native BlueTooth access.
-I of course wanted to automate uploading to the App Store, and typically I would just use Fastlane, but this client
-doesn’t have iOS devs, and they most likely have no interest in keeping Fastlane’s gem up to date, and very likely have no interest in keeping
-Ruby up to date since they weren’t using Ruby for anything else.
And personally, my projects don’t have Ruby backends either now that I’ve moved to Server-side Swift. So I don’t want to deal with Ruby if possible, either.
-So I looked into how to automatically upload to TestFlight without Fastlane, and it was much easier than I expected. It was embarrassingly easier than I expected. It’s 4 commands.
-The four steps are:
-.ipa
There’s a little bit of housekeeping around that: making a temporary directory to store the build artifacts. I chose .build
here, but of course you can choose whatever you like.
#!/bin/bash
-
-if [ -z $APP_LOADER_USERNAME ]; then
- echo "need app loader username stored in \$APP_LOADER_USERNAME"
- exit 1
-fi
-
-if [ -z $APP_LOADER_PASSWORD ]; then
- echo "need app loader password stored in \$APP_LOADER_PASSWORD\nGenerate an app-specific password at appleid.apple.com"
- exit 1
-fi
-
-function version_bump {
- xcrun agvtool next-version -all
-}
-
-function archive_project {
- xcodebuild -project ExampleApp.xcodeproj -scheme ExampleApp \
- -sdk iphoneos archive -archivePath ./.build/ExampleApp.xcarchive \
- -allowProvisioningUpdates -allowProvisioningDeviceRegistration
-}
-
-function export_archive {
- xcodebuild -exportArchive -archivePath ./.build/ExampleApp.xcarchive \
- -exportOptionsPlist appstore-export-options.plist \
- -exportPath ./.build \
- -allowProvisioningUpdates -allowProvisioningDeviceRegistration
-}
-
-function upload {
- echo "=== uploading ExampleApp to App Store ===" && \
- xcrun altool --upload-app -t ios \
- -f ./.build/ExampleApp.ipa \
- -u $APP_LOADER_USERNAME -p $APP_LOADER_PASSWORD && \
- echo "Uploaded ExampleApp build $(xcrun agvtool what-version -terse) of version $(xcrun agvtool what-marketing-version -terse1)"
-}
-
-mkdir -p ./.build && \
-version_bump && \
-archive_project && \
-export_archive && \
-upload
-You’ll also need a file with the export options. In the script above, it’s called appstore-export-options.plist
. And you can stick it wherever.
-In this example, it’s in the root directory of the project.
-The file looks like this:
<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
- <key>method</key>
- <string>app-store</string>
- <key>teamID</key>
- <string>XXXXXXXXXX</string>
- <key>generateAppStoreInformation</key>
- <true/>
- <key>uploadBitcode</key>
- <true/>
- <key>uploadSymbols</key>
- <true/>
-</dict>
-</plist>
-ExampleApp.xcodeproj
to whatever you’re using. If you’re using a workspace, then pass along -workspace ExampleWorkspace.xcworkspace
to the archive command.ExampleApp
scheme name to whatever you’re using.appstore-export-options.plist
$APP_LOADER_USERNAME
and $APP_LOADER_PASSWORD
.
-Be sure to set them in your environment before running the script, however you store them. They are used by the app loader tool to upload the .ipa
to Apple.
-The script checks that username and password environment variables are set
-at the beginning of the script so that the whole build succeeds just to find that the username and password are missing.If you want to use agvtool
to update your build numbers, then you’ll need to enable it in your project settings.
-Here is a lengthy guide to agvtool and how to
-enable it on your project. I recommend using agvtool
since it comes with Xcode, there’s no additional dependencies.
The first time you run this script, you may get a password prompt saying something like “codesign would like to access some keys in your keychain.” -I always hit “Always allow” so that it doesn’t bug me any more.
-Now you have a short script that builds and uploads your app to the App Store. You don’t have to worry about Ruby or Ruby Gems anymore. -And since bash is already on every Mac, and Xcode is already needed for iOS development, there’s no additional dependencies to download for development, or during CI/CD build.
-I learned a lot of this stuff from this article -back when I was first researching how to do this for my client. That article hasn’t been updated in a while, and so I can’t just send people directly to that article. -That’s why I wrote this one.
-This post will show how to use Docker layers for Swift Package Manager projects. The sample project is on GitHub.
-Consider this Dockerfile for a Swift project:
-FROM swift:5.3.1-focal
-WORKDIR /build
-ADD ./Package.* ./
-ADD ./Sources ./Sources
-RUN swift build -c release
-ENV PORT 80
-EXPOSE $PORT
-CMD /app/Run serve --env production --hostname 0.0.0.0 -p $PORT
-We would build it using docker build -t hello-world .
and we would see the following output:
=> [internal] load .dockerignore 0.0s
- => => transferring context: 103B 0.0s
- => [internal] load build definition from Dockerfile 0.0s
- => => transferring dockerfile: 503B 0.0s
- => [internal] load metadata for docker.io/library/swift:5.3.1-focal 0.6s
- => [1/5] FROM docker.io/library/swift:5.3.1-focal@sha256:3fff4e7b806d04e1f8ef4d8f15eabcd8c9d8898f5333cc59aa3fc2f527ce96a9 36.3s
- => => resolve docker.io/library/swift:5.3.1-focal@sha256:3fff4e7b806d04e1f8ef4d8f15eabcd8c9d8898f5333cc59aa3fc2f527ce96a9 0.0s
- => => sha256:3fff4e7b806d04e1f8ef4d8f15eabcd8c9d8898f5333cc59aa3fc2f527ce96a9 320B / 320B 0.0s
- => => sha256:a0f91b147671afee61dec3eb0f6cedfd819732931a8e5dc706a79a65509f3b58 1.37kB / 1.37kB 0.0s
- => => sha256:e07bbf2adda7980b0d48d09a34922356fc699409bf384c5532540a3e6739cb2c 7.13kB / 7.13kB 0.0s
- => => sha256:6a5697faee43339ef8e33e3839060252392ad99325a48f7c9d7e93c22db4d4cf 28.56MB / 28.56MB 2.8s
- => => sha256:ba13d3bc422b493440f97a8f148d245e1999cb616cb05876edc3ef29e79852f2 847B / 847B 0.2s
- => => sha256:a254829d9e55168306fd80a49e02eb015551facee9c444d9dce3b26d19238b82 162B / 162B 0.2s
- => => sha256:d800a558a8d3578248d40fccef72e826fe3b709469aa49c2f2975ab7ad7ddc98 93.61MB / 93.61MB 7.2s
- => => sha256:ee5f2394c96e071384625f1080ece8feca75f4b7b0118570e82032ee2cc4b131 422.07MB / 422.07MB 17.7s
- => => extracting sha256:6a5697faee43339ef8e33e3839060252392ad99325a48f7c9d7e93c22db4d4cf 1.9s
- => => extracting sha256:ba13d3bc422b493440f97a8f148d245e1999cb616cb05876edc3ef29e79852f2 0.0s
- => => extracting sha256:a254829d9e55168306fd80a49e02eb015551facee9c444d9dce3b26d19238b82 0.0s
- => => extracting sha256:d800a558a8d3578248d40fccef72e826fe3b709469aa49c2f2975ab7ad7ddc98 7.7s
- => => extracting sha256:ee5f2394c96e071384625f1080ece8feca75f4b7b0118570e82032ee2cc4b131 17.6s
- => [internal] load build context 0.0s
- => => transferring context: 5.55kB 0.0s
- => [2/5] WORKDIR /build 0.7s
- => [3/5] ADD ./Package.* ./ 0.1s
- => [4/5] ADD ./Sources ./Sources 0.0s
- => [5/5] RUN swift build -c release 291.9s
- => exporting to image 3.5s
- => => exporting layers 3.4s
- => => writing image sha256:fda601b5aebba754ac82c1a5dc95f9ad0be6b7695340d3355a9bab26d6c70237 0.1s
- => => naming to docker.io/library/hello-world 0.0s
-We can see the project builds for over 300 seconds or 5 minutes.
-When building this project, there’s only one line that builds any Swift code: RUN swift build -c release
. Therefore only one Docker layer cache exists.
-So anytime you change your code, in your own Sources
directory, your third party dependencies rebuild your entire project without any cache.
Ideally, the third party dependencies would download and cache separately from your own code. For example the web framework Vapor has many dependencies, -and compiling just the framework takes a long time.
-If we create a empty files in a directory structure that matches our Sources
folder, we can build the project’s dependencies. And docker will cache the built dependencies into a separate layer in our Dockerfile
. This will speed up subsequent builds.
And now our dockerfile could look like this:
-FROM swift:5.3.1-focal
-WORKDIR /build
-ADD ./Package.* ./ # A
-RUN swift package resolve # B
-RUN mkdir -p Sources/App && touch Sources/App/empty.swift \
- && mkdir -p Sources/Run && touch Sources/Run/main.swift # C
-RUN swift build -c release # D
-ADD ./Sources ./Sources # E
-RUN swift build -c release # F
-ENV PORT 80
-EXPOSE $PORT
-CMD /app/Run serve --env production --hostname 0.0.0.0 -p $PORT
-On line # A
, we add the Package.swift
and Package.resolved
files as normal. On line # B
, we resolve all the dependencies.
If stop editing here, and we swift build
now, we’ll get an error about missing sources, since we haven’t copied our Sources
in yet.
-To solve this, we write line # C
to create the needed directories and create empty files in them. Note that empty file the Run
directory needs to be named main.swift
since it’s an executable target.
On line # D
, we build all our dependencies.
On lines # E
and # F
, we add our sources and build then as normal.
Now when we run the docker build, the output of docker build -t hello-world .
hasn’t changed much, because nothing has been cached yet. However, the next time we build (after we make some small code changes - for example, change from “hello world” to “hello earth”),
-the output of docker build -t hello-world .
looks like this:
[+] Building 4.8s (13/13) FINISHED
- => [internal] load build definition from Dockerfile 0.0s
- => => transferring dockerfile: 37B 0.0s
- => [internal] load .dockerignore 0.0s
- => => transferring context: 34B 0.0s
- => [internal] load metadata for docker.io/library/swift:5.3.1-focal 0.5s
- => [1/8] FROM docker.io/library/swift:5.3.1-focal@sha256:3fff4e7b806d04e1f8ef4d8f15eabcd8c9d8898f5333cc59aa3fc2f527ce96a9 0.0s
- => [internal] load build context 0.0s
- => => transferring context: 461B 0.0s
- => CACHED [2/8] WORKDIR /build 0.0s
- => CACHED [3/8] ADD ./Package.* ./ 0.0s
- => CACHED [4/8] RUN swift package resolve 0.0s
- => CACHED [5/8] RUN mkdir -p Sources/App && touch Sources/App/empty.swift && mkdir -p Sources/Deps && touch Sources/Deps/ 0.0s
- => CACHED [6/8] RUN swift build -c release --target Deps 0.0s
- => [7/8] ADD ./Sources ./Sources 0.1s
- => [8/8] RUN swift build -c release 3.8s
- => exporting to image 0.3s
- => => exporting layers 0.3s
- => => writing image sha256:5960e06c78c5c2e247a42bff00c390ffcf75093e546228e8aa4cbd46ce37e5e1 0.0s
- => => naming to docker.io/library/hello-world 0.0s
-We can see that it builds much quicker! Only a few seconds compared to the over 300 seconds before caching.
-Also, we can see the various layers marked as CACHED
. Only our own project’s code needs to be compiled, since the third party dependencies already are compiled and cached in the docker layer.
I hope this was helpful. Checkout the sample project on Github here https://github.com/danramteke/spm-in-docker
-Here’s how I use Xcodegen for a new project that targets MacOS and iOS. It’s here for your reference or inspiration.
-Here is my project.yml
:
name: Example
-options:
- deploymentTarget:
- iOS: "14.0"
- macOS: "11.0"
-schemes:
- Example-iOS:
- build:
- targets:
- Example-iOS: all
- Example-macOS:
- build:
- targets:
- Example-macOS: all
-targets:
- Example-macOS:
- type: application
- platform: macOS
- sources:
- - path: macOS
- - path: Shared
- settings:
- DEVELOPMENT_TEAM: 1234567890
- CODE_SIGN_STYLE: Automatic
- ORGANIZATIONNAME: "Example Organization"
- PRODUCT_BUNDLE_IDENTIFIER: com.Example.app
- VERSIONING_SYSTEM: apple-generic
- SWIFT_VERSION: 5.3
- CURRENT_PROJECT_VERSION: ${APP_BUILD_NUMBER}
- entitlements:
- path: macOS/macOS.entitlements
- properties:
- com.apple.security.app-sandbox: true
- com.apple.security.files.user-selected.read-only: true
- info:
- path: macOS/Info.plist
- properties:
- CFBundleVersion: ${APP_BUILD_NUMBER}
- CFBundleShortVersionString: ${APP_BUILD_VERSION}
- CFBundleDevelopmentRegion: $(DEVELOPMENT_LANGUAGE)
- CFBundleName: $(PRODUCT_NAME)
- CFBundleDisplayName: Example
- CFBundleIconFile: ""
- CFBundleIdentifier: $(PRODUCT_BUNDLE_IDENTIFIER)
- CFBundleInfoDictionaryVersion: "6.0"
- CFBundlePackageType: $(PRODUCT_BUNDLE_PACKAGE_TYPE)
- LSMinimumSystemVersion: $(MACOSX_DEPLOYMENT_TARGET)
- Example-iOS:
- type: application
- platform: iOS
- sources:
- - path: iOS
- - path: Shared
- settings:
- DEVELOPMENT_TEAM: 1234567890
- CODE_SIGN_STYLE: Automatic
- ORGANIZATIONNAME: "Example Organization"
- PRODUCT_BUNDLE_IDENTIFIER: com.Example.app
- TARGETED_DEVICE_FAMILY: 1,2
- VERSIONING_SYSTEM: apple-generic
- SWIFT_VERSION: 5.3
- CURRENT_PROJECT_VERSION: ${APP_BUILD_NUMBER}
- entitlements:
- path: iOS/iOS.entitlements
- info:
- path: iOS/Info.plist
- properties:
- CFBundleVersion: ${APP_BUILD_NUMBER}
- CFBundleShortVersionString: ${APP_BUILD_VERSION}
- CFBundleDevelopmentRegion: $(DEVELOPMENT_LANGUAGE)
- CFBundleDisplayName: Example
- ITSAppUsesNonExemptEncryption: false
- LSRequiresIPhoneOS: true
- UIApplicationSceneManifest:
- UIApplicationSupportsMultipleScenes: true
- UIApplicationSupportsIndirectInputEvents: true
- UILaunchScreen: {}
- NSAppTransportSecurity:
- NSAllowsLocalNetworking: true
- UIRequiredDeviceCapabilities: [armv7]
- UISupportedInterfaceOrientations:
- - UIInterfaceOrientationPortrait
- UISupportedInterfaceOrientations~ipad:
- - UIInterfaceOrientationPortrait
- - UIInterfaceOrientationPortraitUpsideDown
- - UIInterfaceOrientationLandscapeLeft
- - UIInterfaceOrientationLandscapeRight
-This defines two targets, one for macOS and one for iOS, just like Xcode’s multiplatform template.
-I don’t call xcodegen
directly. The project.yml
references two variables for the build number and the marketing version, and those are stored in two files named build-number
which contains the current build number (for example: 1
) and build-version
which contains the current marketing version (for example: 1.0.0
).
To make sure those values are available to xcodegen
, I create a bootstrap.sh
script that looks like this:
rm -fr *.xcodeproj
-
-export APP_BUILD_NUMBER=$(cat build-number)
-export APP_BUILD_VERSION=$(cat build-version)
-
-xcodegen generate -s project.yml
-So when I need to regenerate the xcode project, I run ./bootstrap.sh
from the project’s root directory.
When you’re ready to submit a build to TestFlight, I recommend tagging that commit in the style of 1.2.3-7
, where 1.2.3
is the marketing version shown on the App Store and 7
is the build number. This strategy allows us to easily reference what code went into which build.
If we take this approach, we can set up our CI/CD system to build our code and push to the App Store.
-On BitRise, we can use the following code to interpret the tag as as a marketing version and build number. This code uses Bash regular expressions.
-#!/usr/bin/env bash
-set -e set -o pipefail set -x
-
-echo "tag is: $BITRISE_GIT_TAG"
-rx='([0-9]+\.[0-9]+\.[0-9]+)-([0-9]+)'
-if [[ $BITRISE_GIT_TAG =~ $rx ]]; then
- echo "0: ${BASH_REMATCH[0]}"
- echo "1: ${BASH_REMATCH[1]}"
- echo "2: ${BASH_REMATCH[2]}"
-else
- echo "No match!"
- exit 1
-fi
-envman add --key GIT_TAG_MARKETING_VERSION --value "${BASH_REMATCH[1]}"
-envman add --key GIT_TAG_BUILD_NUMBER --value "${BASH_REMATCH[2]}"
-If this script is run as a “build step”, we use BitRise’s envman
to add the parsed information to our environment, so that our other build steps have easy access to this information.
For small projects, when the cost of a load balancer can cost as much as a smaller node, a load balancer can feel like overkill. I would prefer to have another compute node instead! But I still want the other benefits of Kubernetes. This is the work around I’ve done on for DigitalOcean (referral link)’s Kubernetes aka DOKS.
-Start with the raw Contour yaml available from their GitHub: v1.22.1/…/contour.yaml then search for a Service
with name: envoy
,
For the Service
, change the type
from LoadBalancer
to NodePort
.
If you only have one node, you’re done! You can point your DigitalOcean (referral link) ReservedIP
to this node.
If you have more than one Node, I use a nodeSelector
and a label to specify an “edge” node.
Find a DaemonSet
with name: envoy
, add a nodeSelector
key to the spec.template.spec
dictionary. It will be a sibling of the containers
key. It could look like this:
spec:
- # …
- template:
- # …
- spec:
- nodeSelector:
- node-role: edge
- containers:
- # …
-Then, looking at your cluster, find the Node
you want to use as the edge node. And run something like this kubectl label node $edgeNodeName node-role=edge --overwrite
You can now point your DigitalOcean (referral link) ReservedIP
to this node.
Now you can apply your modified contour.yaml
! Now you have a nice Ingress controller with nice features. You can spend your budget on compute instead of a load balancer.
This is a more brittle setup for sure. If DigitalOcean (referral link) ever recreates your cluster for you, they will usually create a Load Balancer for you. Just remember to delete it.
-Also, this setup can’t really do in-place upgrades. So instead, when you need to upgrade your Kubernetes version, setup a second cluster with your apps, swap your ReservedIP to that cluster, then delete your old one.
- -Contact me daniel at danramteke dot com
Wrangling iOS build scripts and optimizing server infrastructure scripts: I enjoy both of these. If it can be automated with a script, it probably should. Scripting as much as possible increases confidence in important processes such as publishing to the App Store or disaster recovery. - Unfortunately, not everyone has the time or familiarity to develop bullet-proof scripts. Contracting a DevOps consultant such as myself can propel your project forward.
- - Docker is a format for packing apps into standardized containers. Standardized and lightweight containers enables new workflows for codesharing, quality testing, and dev enviroments. Docker shines in production enviroments when combined with an orchestration layer such as Kubernetes. -
- - -- Terraform is a declarative tool for managing infrastructure. And it's a gamechanger. It has eclipsed Ansible in my workflows. It also enables reuse with its modules feature. +
Wrangling iOS build scripts and optimizing server infrastructure scripts: I enjoy both of these. If it can be automated with a script, it probably should. Scripting as much as possible increases confidence in important processes such as publishing to the App Store or disaster recovery. + Unfortunately, not everyone has the time or familiarity to develop bullet-proof scripts. Contracting a DevOps consultant such as myself can propel your project forward.
+Docker is a format for packing apps into standardized containers. Standardized and lightweight containers enables new workflows for codesharing, quality testing, and dev enviroments. Docker shines in production enviroments when combined with an orchestration layer such as Kubernetes. +
+Terraform is a declarative tool for managing infrastructure. And it's a gamechanger. It has eclipsed Ansible in my workflows. It also enables reuse with its modules feature. I even have a merged pull request to DigitalOcean's Terraform Provider. -
- - -- After declaring infra with Terraform and packing apps with Docker, Kubernetes is the declarative glue between the two. +