@mostrom/app-shell 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,69 +14,50 @@ From `platform/shared/packages/app-shell`:
14
14
  Default scope is `@mostrom` (override with `--scope` if needed).
15
15
 
16
16
  ```bash
17
- # Dry run
18
- npm run publish:npm:dry-run -- --version 0.1.0
19
- # or: sh scripts/publish-npm.sh --version 0.1.0 --dry-run
17
+ # Dry run (auto-increment patch version)
18
+ npm run publish:npm:dry-run
19
+ # or: sh scripts/publish-npm.sh --dry-run
20
20
 
21
- # Publish to latest
21
+ # Publish to latest (auto-increment patch version)
22
+ npm run publish:npm
23
+ # or: sh scripts/publish-npm.sh
24
+
25
+ # First run or explicit override
22
26
  npm run publish:npm -- --version 0.1.0
23
27
  # or: sh scripts/publish-npm.sh --version 0.1.0
24
28
 
25
29
  # Publish to custom tag
26
- npm run publish:npm -- --version 0.1.0 --tag next
27
- # or: sh scripts/publish-npm.sh --version 0.1.0 --tag next
30
+ npm run publish:npm -- --tag next
31
+ # or: sh scripts/publish-npm.sh --tag next
28
32
  ```
29
33
 
30
- The script reads `NODE_AUTH_TOKEN` from environment variables, and also checks `app-shell/.env` for that key.
34
+ The script reads `NODE_AUTH_TOKEN` from environment variables, and also checks `app-shell/.env` for that key. It stores the last successful version in `scripts/.publish-version` and auto-increments patch versions on subsequent runs.
31
35
 
32
36
  ## Quick Start
33
37
 
34
- ### Initial Setup (New Apps)
35
-
36
- Run the init script to configure a new app to use app-shell:
37
-
38
- ```bash
39
- # From your app's frontend directory, run the init script directly
40
- node ../../../shared/packages/app-shell/bin/init.js
41
-
42
- # Force overwrite existing files
43
- node ../../../shared/packages/app-shell/bin/init.js --force
38
+ ### Setup (No Init Script)
44
39
 
45
- # Alternative: If app-shell is linked in node_modules, use the bin
46
- ./node_modules/.bin/app-shell-init
47
- ```
40
+ App setup is now file-based and does not require running an init command.
48
41
 
49
- > **Note:** In this monorepo, prefer running the local init script directly. If you publish to npm, consumers can also use the installed bin (`app-shell-init`).
42
+ Required app files:
50
43
 
51
- The init script creates/updates:
52
-
53
- - `app/tailwind.css` - Imports app-shell styles
54
- - `tailwind.config.ts` - Configured to scan app-shell components
55
- - `components.json` - shadcn/ui configuration
56
- - `app/lib/utils.ts` - Utility functions (cn, getBasePathHref)
57
-
58
- ### When to Run Init
59
-
60
- | Scenario | Run Init? |
61
- | ------------------------------------------- | ---------------------------------------------------- |
62
- | Setting up a new app | Yes |
63
- | CSS changes in app-shell | No - changes are picked up automatically |
64
- | Adding new shadcn components | No - use `npx shadcn@latest add <component>` |
65
- | Tailwind not scanning app-shell classes | Yes, with `--force` to regenerate tailwind.config.ts |
66
- | Upgrading app-shell with new required files | Yes, with `--force` |
44
+ - `app/tailwind.css`
45
+ - `tailwind.config.ts`
46
+ - `components.json`
47
+ - `app/lib/utils.ts`
48
+ - `vite.config.ts` using `getSharedViteConfig()` from `@platform/app-shell/vite`
67
49
 
68
50
  ### Troubleshooting
69
51
 
70
52
  **Styles not applying from app-shell:**
71
53
 
72
- 1. Ensure `tailwind.config.ts` includes the app-shell path in `content`:
54
+ 1. Ensure `tailwind.config.ts` includes app-shell content:
73
55
  ```ts
74
56
  content: [
75
57
  "./app/**/*.{ts,tsx,js,jsx}",
76
- "../../../shared/packages/app-shell/src/**/*.{ts,tsx}",
58
+ "./node_modules/@platform/app-shell/src/**/*.{ts,tsx}",
77
59
  ],
78
60
  ```
79
- 2. Run `node ../../../shared/packages/app-shell/bin/init.js --force` to regenerate config files
80
61
 
81
62
  **Conflicting CSS (wrong colors, broken layout):**
82
63
 
@@ -158,13 +139,7 @@ The App Shell builds the **All services** menu and global search suggestions fro
158
139
  `@mostrom/service-catalog`. Until we move to a monorepo workspace setup, each
159
140
  app should explicitly depend on this package to avoid module resolution issues.
160
141
 
161
- Add this to your app's `package.json` dependencies:
162
-
163
- ```json
164
- {
165
- "@mostrom/service-catalog": "file:../../../shared/packages/service-catalog"
166
- }
167
- ```
142
+ When consuming published packages, install `@platform/service-catalog` as an npm alias to `@mostrom/service-catalog`.
168
143
 
169
144
  ## Notes
170
145
 
package/bun.lock CHANGED
@@ -35,9 +35,11 @@
35
35
  "vaul": "^1.1.2",
36
36
  "zod": "^4.3.6",
37
37
  },
38
+ "devDependencies": {
39
+ "@types/react": "19.2.14",
40
+ "@types/react-dom": "19.2.3",
41
+ },
38
42
  "peerDependencies": {
39
- "@types/react": "^19.0.0",
40
- "@types/react-dom": "^19.0.0",
41
43
  "react": "^19.0.0",
42
44
  "react-dom": "^19.0.0",
43
45
  },
@@ -242,7 +244,7 @@
242
244
 
243
245
  "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="],
244
246
 
245
- "@types/react": ["@types/react@19.2.13", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ=="],
247
+ "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
246
248
 
247
249
  "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
248
250
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mostrom/app-shell",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -35,6 +35,7 @@
35
35
  "@dnd-kit/utilities": "^3.2.2",
36
36
  "@floating-ui/dom": "^1.7.5",
37
37
  "@hookform/resolvers": "^5.2.2",
38
+ "@platform/service-catalog": "npm:@mostrom/service-catalog@^0.1.2",
38
39
  "@radix-ui/react-dropdown-menu": "^2.1.4",
39
40
  "@tanstack/react-table": "^8.21.3",
40
41
  "class-variance-authority": "^0.7.1",
@@ -53,8 +54,7 @@
53
54
  "tailwind-merge": "^3.4.0",
54
55
  "tw-animate-css": "^1.4.0",
55
56
  "vaul": "^1.1.2",
56
- "zod": "^4.3.6",
57
- "@mostrom/service-catalog": "^0.1.0"
57
+ "zod": "^4.3.6"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/react": "19.2.14",
@@ -4,14 +4,15 @@ set -eu
4
4
  usage() {
5
5
  cat <<USAGE
6
6
  Usage:
7
- ./scripts/publish-npm.sh --version <version> [--scope <scope>] [--tag <tag>] [--otp <code>] [--dry-run]
8
- ./scripts/publish-npm.sh <version> [--scope <scope>] [--tag <tag>] [--otp <code>] [--dry-run]
7
+ ./scripts/publish-npm.sh [--version <version>] [--scope <scope>] [--tag <tag>] [--otp <code>] [--dry-run]
8
+ ./scripts/publish-npm.sh [<version>] [--scope <scope>] [--tag <tag>] [--otp <code>] [--dry-run]
9
9
 
10
10
  Examples:
11
+ ./scripts/publish-npm.sh
11
12
  ./scripts/publish-npm.sh 0.1.0
12
13
  ./scripts/publish-npm.sh --version 0.1.0 --scope @mostrom --tag next
13
14
  ./scripts/publish-npm.sh --version 0.1.0 --scope mostrom --otp 123456
14
- ./scripts/publish-npm.sh --version 0.1.0 --dry-run
15
+ ./scripts/publish-npm.sh --dry-run
15
16
  USAGE
16
17
  }
17
18
 
@@ -19,6 +20,7 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
19
20
  APP_SHELL_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
20
21
  PACKAGES_DIR="$(cd "${APP_SHELL_DIR}/.." && pwd)"
21
22
  SERVICE_CATALOG_DIR="${PACKAGES_DIR}/service-catalog"
23
+ VERSION_STATE_FILE="${SCRIPT_DIR}/.publish-version"
22
24
 
23
25
  VERSION=""
24
26
  SCOPE="@mostrom"
@@ -69,12 +71,6 @@ while [ "$#" -gt 0 ]; do
69
71
  esac
70
72
  done
71
73
 
72
- if [ -z "$VERSION" ]; then
73
- echo "Error: version is required." >&2
74
- usage
75
- exit 1
76
- fi
77
-
78
74
  case "$SCOPE" in
79
75
  @*) ;;
80
76
  *) SCOPE="@$SCOPE" ;;
@@ -89,13 +85,61 @@ fi
89
85
  SERVICE_CATALOG_PACKAGE_NAME="${SCOPE}/service-catalog"
90
86
  APP_SHELL_PACKAGE_NAME="${SCOPE}/app-shell"
91
87
 
88
+ bump_patch_version() {
89
+ current_version="$1"
90
+ CURRENT_VERSION="$current_version" node - <<'NODE'
91
+ const value = process.env.CURRENT_VERSION;
92
+ if (!/^\d+\.\d+\.\d+$/.test(value || "")) {
93
+ process.exit(1);
94
+ }
95
+ const [major, minor, patch] = value.split('.').map(Number);
96
+ process.stdout.write(`${major}.${minor}.${patch + 1}`);
97
+ NODE
98
+ }
99
+
100
+ infer_next_version() {
101
+ if [ -f "$VERSION_STATE_FILE" ]; then
102
+ last_version="$(tr -d '[:space:]' < "$VERSION_STATE_FILE")"
103
+ if [ -z "$last_version" ]; then
104
+ echo "Error: $VERSION_STATE_FILE is empty. Pass --version explicitly once." >&2
105
+ exit 1
106
+ fi
107
+ next_version="$(bump_patch_version "$last_version" || true)"
108
+ if [ -z "$next_version" ]; then
109
+ echo "Error: invalid version in $VERSION_STATE_FILE ($last_version). Pass --version explicitly once." >&2
110
+ exit 1
111
+ fi
112
+ echo "$next_version"
113
+ return 0
114
+ fi
115
+
116
+ latest_registry_version="$(npm view "$SERVICE_CATALOG_PACKAGE_NAME" version 2>/dev/null || true)"
117
+ if [ -n "$latest_registry_version" ]; then
118
+ next_version="$(bump_patch_version "$latest_registry_version" || true)"
119
+ if [ -z "$next_version" ]; then
120
+ echo "Error: failed to parse latest registry version ($latest_registry_version). Pass --version explicitly." >&2
121
+ exit 1
122
+ fi
123
+ echo "$next_version"
124
+ return 0
125
+ fi
126
+
127
+ echo "Error: Could not infer next version. Pass --version explicitly once (for example: --version 0.1.0)." >&2
128
+ exit 1
129
+ }
130
+
131
+ if [ -z "$VERSION" ]; then
132
+ VERSION="$(infer_next_version)"
133
+ echo "==> Auto-incremented version: ${VERSION}"
134
+ fi
135
+
92
136
  TMP_DIR="$(mktemp -d)"
93
137
  cleanup() {
94
138
  rm -rf "$TMP_DIR"
95
139
  }
96
140
  trap cleanup 0 1 2 3 15
97
141
 
98
- if [ -f "${APP_SHELL_DIR}/.env" ]; then
142
+ if [ -z "${NODE_AUTH_TOKEN:-}" ] && [ -f "${APP_SHELL_DIR}/.env" ]; then
99
143
  token_line="$(grep -E '^(export[[:space:]]+)?NODE_AUTH_TOKEN=' "${APP_SHELL_DIR}/.env" | tail -n 1 || true)"
100
144
  if [ -n "$token_line" ]; then
101
145
  token_value="$(printf '%s' "$token_line" | sed -E 's/^(export[[:space:]]+)?NODE_AUTH_TOKEN=//')"
@@ -126,6 +170,7 @@ copy_pkg() {
126
170
  --exclude .env \
127
171
  --exclude test-results \
128
172
  --exclude .DS_Store \
173
+ --exclude scripts/.publish-version \
129
174
  "$from_dir/" "$to_dir/"
130
175
  }
131
176
 
@@ -135,12 +180,14 @@ rewrite_manifest() {
135
180
  package_name="$3"
136
181
  service_catalog_package="${4:-}"
137
182
  service_catalog_range="${5:-}"
183
+ service_catalog_alias="${6:-}"
138
184
 
139
185
  MANIFEST_PATH="$manifest_path" \
140
186
  PACKAGE_VERSION="$package_version" \
141
187
  PACKAGE_NAME="$package_name" \
142
188
  SERVICE_CATALOG_PACKAGE="$service_catalog_package" \
143
189
  SERVICE_CATALOG_RANGE="$service_catalog_range" \
190
+ SERVICE_CATALOG_ALIAS="$service_catalog_alias" \
144
191
  node - <<'NODE'
145
192
  const fs = require('fs');
146
193
  const p = process.env.MANIFEST_PATH;
@@ -148,15 +195,18 @@ const version = process.env.PACKAGE_VERSION;
148
195
  const packageName = process.env.PACKAGE_NAME;
149
196
  const serviceCatalogPackage = process.env.SERVICE_CATALOG_PACKAGE;
150
197
  const serviceCatalogRange = process.env.SERVICE_CATALOG_RANGE;
198
+ const serviceCatalogAlias = process.env.SERVICE_CATALOG_ALIAS;
151
199
  const pkg = JSON.parse(fs.readFileSync(p, 'utf8'));
152
200
  pkg.name = packageName;
153
201
  pkg.version = version;
154
202
  pkg.private = false;
155
203
  pkg.publishConfig = { ...(pkg.publishConfig || {}), access: 'public' };
156
- if (pkg.dependencies && pkg.dependencies['@platform/service-catalog']) {
157
- delete pkg.dependencies['@platform/service-catalog'];
158
- }
159
- if (serviceCatalogPackage && serviceCatalogRange) {
204
+ if (serviceCatalogAlias) {
205
+ pkg.dependencies = { ...(pkg.dependencies || {}), ['@platform/service-catalog']: serviceCatalogAlias };
206
+ if (serviceCatalogPackage && pkg.dependencies && pkg.dependencies[serviceCatalogPackage]) {
207
+ delete pkg.dependencies[serviceCatalogPackage];
208
+ }
209
+ } else if (serviceCatalogPackage && serviceCatalogRange) {
160
210
  pkg.dependencies = { ...(pkg.dependencies || {}), [serviceCatalogPackage]: serviceCatalogRange };
161
211
  }
162
212
  fs.writeFileSync(p, JSON.stringify(pkg, null, 2) + '\n');
@@ -171,7 +221,7 @@ copy_pkg "$SERVICE_CATALOG_DIR" "$SERVICE_TMP_DIR"
171
221
  copy_pkg "$APP_SHELL_DIR" "$APP_SHELL_TMP_DIR"
172
222
 
173
223
  rewrite_manifest "${SERVICE_TMP_DIR}/package.json" "$VERSION" "$SERVICE_CATALOG_PACKAGE_NAME"
174
- rewrite_manifest "${APP_SHELL_TMP_DIR}/package.json" "$VERSION" "$APP_SHELL_PACKAGE_NAME" "$SERVICE_CATALOG_PACKAGE_NAME" "^${VERSION}"
224
+ rewrite_manifest "${APP_SHELL_TMP_DIR}/package.json" "$VERSION" "$APP_SHELL_PACKAGE_NAME" "$SERVICE_CATALOG_PACKAGE_NAME" "^${VERSION}" "npm:${SERVICE_CATALOG_PACKAGE_NAME}@^${VERSION}"
175
225
 
176
226
  publish_pkg() {
177
227
  pkg_dir="$1"
@@ -196,7 +246,14 @@ publish_pkg() {
196
246
  publish_pkg "$SERVICE_TMP_DIR" "$SERVICE_CATALOG_PACKAGE_NAME"
197
247
  publish_pkg "$APP_SHELL_TMP_DIR" "$APP_SHELL_PACKAGE_NAME"
198
248
 
249
+ if [ "$DRY_RUN" -eq 0 ]; then
250
+ printf "%s\n" "$VERSION" > "$VERSION_STATE_FILE"
251
+ fi
252
+
199
253
  echo "==> Done"
200
254
  echo "Published version: ${VERSION}"
201
255
  echo "Scope: ${SCOPE}"
202
256
  echo "Tag: ${TAG}"
257
+ if [ "$DRY_RUN" -eq 0 ]; then
258
+ echo "Version state file: ${VERSION_STATE_FILE}"
259
+ fi
@@ -1,8 +1,8 @@
1
1
  import * as React from "react";
2
2
  import * as Popover from "@radix-ui/react-popover";
3
- import Badge from "@cloudscape-design/components/badge";
4
- import Button from "@cloudscape-design/components/button";
5
- import SpaceBetween from "@cloudscape-design/components/space-between";
3
+ import { Badge } from "../ui/badge";
4
+ import { Button } from "../ui/button";
5
+ import { SpaceBetween } from "../ui/space-between";
6
6
  import { format, isToday, isTomorrow, isValid, parseISO } from "date-fns";
7
7
  import {
8
8
  DateSelector,
@@ -1,5 +1,5 @@
1
1
  import * as React from "react";
2
- import Badge from "@cloudscape-design/components/badge";
2
+ import { Badge } from "../ui/badge";
3
3
  import type { Assignee, ColumnDefinition, TableBadgeColor } from "./types";
4
4
  import { AssigneeSelector } from "@/components/ui/assignee-selector";
5
5
 
@@ -0,0 +1,59 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import { Slot } from "radix-ui"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const badgeVariants = cva(
8
+ "inline-flex items-center justify-center rounded-full border border-transparent px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
13
+ secondary:
14
+ "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
15
+ destructive:
16
+ "bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
17
+ outline:
18
+ "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
19
+ ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
20
+ link: "text-primary underline-offset-4 [a&]:hover:underline",
21
+ },
22
+ // Color variants for Cloudscape compatibility
23
+ color: {
24
+ blue: "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200",
25
+ green: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
26
+ grey: "bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200",
27
+ red: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200",
28
+ },
29
+ },
30
+ defaultVariants: {
31
+ variant: "default",
32
+ },
33
+ }
34
+ )
35
+
36
+ function Badge({
37
+ className,
38
+ variant,
39
+ color,
40
+ asChild = false,
41
+ ...props
42
+ }: React.ComponentProps<"span"> &
43
+ VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
44
+ const Comp = asChild ? Slot.Root : "span"
45
+
46
+ // If color is provided, use color variant; otherwise use variant (defaulting to "default")
47
+ const resolvedVariant = color ? undefined : (variant ?? "default")
48
+
49
+ return (
50
+ <Comp
51
+ data-slot="badge"
52
+ data-variant={color ?? variant}
53
+ className={cn(badgeVariants({ variant: resolvedVariant, color }), className)}
54
+ {...props}
55
+ />
56
+ )
57
+ }
58
+
59
+ export { Badge, badgeVariants }
@@ -19,6 +19,10 @@ const buttonVariants = cva(
19
19
  ghost:
20
20
  "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
21
21
  link: "text-primary underline-offset-4 hover:underline",
22
+ // Cloudscape-compatible variants
23
+ primary: "bg-primary text-primary-foreground hover:bg-primary/90",
24
+ normal:
25
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
22
26
  },
23
27
  size: {
24
28
  default: "h-9 px-4 py-2 has-[>svg]:px-3",
@@ -43,10 +47,12 @@ function Button({
43
47
  variant = "default",
44
48
  size = "default",
45
49
  asChild = false,
50
+ fullWidth = false,
46
51
  ...props
47
52
  }: React.ComponentProps<"button"> &
48
53
  VariantProps<typeof buttonVariants> & {
49
54
  asChild?: boolean
55
+ fullWidth?: boolean
50
56
  }) {
51
57
  const Comp = asChild ? Slot.Root : "button"
52
58
 
@@ -55,7 +61,7 @@ function Button({
55
61
  data-slot="button"
56
62
  data-variant={variant}
57
63
  data-size={size}
58
- className={cn(buttonVariants({ variant, size, className }))}
64
+ className={cn(buttonVariants({ variant, size, className }), fullWidth && "w-full")}
59
65
  {...props}
60
66
  />
61
67
  )
@@ -1,5 +1,6 @@
1
1
  // UI Components (shadcn/ui + reui)
2
2
  export * from "./avatar";
3
+ export * from "./badge";
3
4
  export * from "./breadcrumb";
4
5
  export * from "./button";
5
6
  export * from "./button-group";
@@ -24,6 +25,7 @@ export * from "./scroll-area";
24
25
  export * from "./select";
25
26
  export * from "./separator";
26
27
  export * from "./sheet";
28
+ export * from "./space-between";
27
29
  export * from "./sidebar";
28
30
  export * from "./skeleton";
29
31
  export * from "./sonner";
@@ -0,0 +1,59 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import { cn } from "@/lib/utils"
4
+
5
+ const spaceBetweenVariants = cva("flex", {
6
+ variants: {
7
+ direction: {
8
+ vertical: "flex-col",
9
+ horizontal: "flex-row",
10
+ },
11
+ size: {
12
+ xxxs: "gap-0.5", // 2px
13
+ xxs: "gap-1", // 4px
14
+ xs: "gap-2", // 8px
15
+ s: "gap-3", // 12px
16
+ m: "gap-4", // 16px
17
+ l: "gap-6", // 24px
18
+ xl: "gap-8", // 32px
19
+ xxl: "gap-10", // 40px
20
+ },
21
+ alignItems: {
22
+ center: "items-center",
23
+ start: "items-start",
24
+ end: "items-end",
25
+ baseline: "items-baseline",
26
+ stretch: "items-stretch",
27
+ },
28
+ },
29
+ defaultVariants: {
30
+ direction: "vertical",
31
+ size: "m",
32
+ alignItems: "stretch",
33
+ },
34
+ })
35
+
36
+ export interface SpaceBetweenProps
37
+ extends React.HTMLAttributes<HTMLDivElement>,
38
+ VariantProps<typeof spaceBetweenVariants> {}
39
+
40
+ function SpaceBetween({
41
+ className,
42
+ direction,
43
+ size,
44
+ alignItems,
45
+ children,
46
+ ...props
47
+ }: SpaceBetweenProps) {
48
+ return (
49
+ <div
50
+ data-slot="space-between"
51
+ className={cn(spaceBetweenVariants({ direction, size, alignItems }), className)}
52
+ {...props}
53
+ >
54
+ {children}
55
+ </div>
56
+ )
57
+ }
58
+
59
+ export { SpaceBetween, spaceBetweenVariants }
package/src/vite.js CHANGED
@@ -1,11 +1,15 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
3
4
 
4
5
  /**
5
6
  * Returns the monorepo root directory (platform/).
6
7
  * @param {string} currentDir
7
8
  * @returns {string}
8
9
  */
10
+ const APP_SHELL_SRC_DIR = path.dirname(fileURLToPath(import.meta.url));
11
+ const APP_SHELL_PACKAGE_DIR = path.resolve(APP_SHELL_SRC_DIR, "..");
12
+
9
13
  function getMonorepoRoot(currentDir) {
10
14
  let dir = currentDir;
11
15
  while (dir !== "/") {
@@ -99,9 +103,9 @@ function ssrCssStubPlugin() {
99
103
  export function getSharedViteConfig(appDir) {
100
104
  const monorepoRoot = getMonorepoRoot(appDir);
101
105
  const appNodeModules = path.resolve(appDir, "node_modules");
102
- const sharedPackages = path.resolve(monorepoRoot, "shared/packages");
103
- const appShellSrc = path.resolve(sharedPackages, "app-shell/src");
104
- const appShellNodeModules = path.resolve(sharedPackages, "app-shell/node_modules");
106
+ const appShellSrc = APP_SHELL_SRC_DIR;
107
+ const packageNodeModules = path.resolve(APP_SHELL_PACKAGE_DIR, "node_modules");
108
+ const appShellNodeModules = fs.existsSync(packageNodeModules) ? packageNodeModules : appNodeModules;
105
109
  const appHasDndKit = fs.existsSync(path.resolve(appNodeModules, "@dnd-kit/core"));
106
110
  const dndKitNodeModules = appHasDndKit ? appNodeModules : appShellNodeModules;
107
111
 
@@ -111,10 +115,6 @@ export function getSharedViteConfig(appDir) {
111
115
  preserveSymlinks: true,
112
116
  alias: [
113
117
  // Source aliases for HMR during development
114
- {
115
- find: /^@platform\/service-catalog(\/.*)?$/,
116
- replacement: `${path.resolve(sharedPackages, "service-catalog/src")}$1`,
117
- },
118
118
  {
119
119
  find: /^@platform\/app-shell(\/.*)?$/,
120
120
  replacement: `${appShellSrc}$1`,
package/bin/init.js DELETED
@@ -1,269 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * @platform/app-shell init
5
- *
6
- * Sets up a new app to use the app-shell package correctly.
7
- * Similar to `shadcn init` but configured for our monorepo structure.
8
- *
9
- * Usage (from your app's frontend directory):
10
- * node ../../../shared/packages/app-shell/bin/init.js
11
- * node ../../../shared/packages/app-shell/bin/init.js --force
12
- *
13
- * Note: npx/bunx won't work - this is a private monorepo package, not published to npm.
14
- *
15
- * What it does:
16
- * 1. Creates minimal app/tailwind.css (delegates to app-shell)
17
- * 2. Creates components.json (shadcn/ui configuration)
18
- * 3. Creates app/lib/utils.ts (cn utility + getBasePathHref)
19
- * 4. Creates tailwind.config.ts
20
- * 5. Validates vite.config.ts uses getSharedViteConfig()
21
- */
22
-
23
- import fs from "node:fs";
24
- import path from "node:path";
25
- import { fileURLToPath } from "node:url";
26
-
27
- const __filename = fileURLToPath(import.meta.url);
28
- const __dirname = path.dirname(__filename);
29
- const cwd = process.cwd();
30
-
31
- // ANSI colors
32
- const green = (s) => `\x1b[32m${s}\x1b[0m`;
33
- const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
34
- const red = (s) => `\x1b[31m${s}\x1b[0m`;
35
- const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
36
- const dim = (s) => `\x1b[2m${s}\x1b[0m`;
37
-
38
- console.log();
39
- console.log(cyan("@platform/app-shell init"));
40
- console.log(dim("Setting up app-shell configuration..."));
41
- console.log();
42
-
43
- // Templates
44
- const TAILWIND_CSS = `@import "https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@300;400;500;600;700&display=swap";
45
- @import "tw-animate-css";
46
- @import "tailwindcss";
47
- @import "@platform/app-shell/styles.css";
48
- @config "../tailwind.config.ts";
49
- `;
50
-
51
- // Compute relative path to app-shell from current directory
52
- function getAppShellRelativePath() {
53
- // The init script is at app-shell/bin/init.js
54
- // So app-shell src is at ../src relative to this script
55
- const appShellSrc = path.resolve(__dirname, "../src");
56
- const relativePath = path.relative(cwd, appShellSrc);
57
- return relativePath.replace(/\\/g, "/"); // Normalize for all platforms
58
- }
59
-
60
- const appShellPath = getAppShellRelativePath();
61
-
62
- const TAILWIND_CONFIG = `export default {
63
- darkMode: "class",
64
- content: [
65
- "./app/**/*.{ts,tsx,js,jsx}",
66
- // Include app-shell components for Tailwind to scan
67
- "${appShellPath}/**/*.{ts,tsx}",
68
- ],
69
- };
70
- `;
71
-
72
- const COMPONENTS_JSON = `{
73
- "$schema": "https://ui.shadcn.com/schema.json",
74
- "style": "new-york",
75
- "rsc": false,
76
- "tsx": true,
77
- "tailwind": {
78
- "config": "tailwind.config.ts",
79
- "css": "app/tailwind.css",
80
- "baseColor": "neutral",
81
- "cssVariables": true,
82
- "prefix": ""
83
- },
84
- "iconLibrary": "lucide",
85
- "rtl": false,
86
- "aliases": {
87
- "components": "~/components",
88
- "utils": "~/lib/utils",
89
- "ui": "~/components/ui",
90
- "lib": "~/lib",
91
- "hooks": "~/hooks"
92
- },
93
- "registries": {}
94
- }
95
- `;
96
-
97
- const UTILS_TS = `import { clsx, type ClassValue } from "clsx"
98
- import { twMerge } from "tailwind-merge"
99
-
100
- export function cn(...inputs: ClassValue[]) {
101
- return twMerge(clsx(inputs))
102
- }
103
-
104
- /**
105
- * Returns a path with the VITE_BASE_PATH prefix for use in href attributes.
106
- * React Router's navigate() handles basename automatically, but href attributes
107
- * on Cloudscape Link components (or native <a> tags) do not.
108
- *
109
- * @param path - The relative path (e.g., "/clients/123" or "/scheduling/abc")
110
- * @returns The path prefixed with the base path (e.g., "/client-management/clients/123")
111
- */
112
- export function getBasePathHref(path: string): string {
113
- const configuredBasePath = import.meta.env.VITE_BASE_PATH;
114
- if (typeof configuredBasePath !== "string") {
115
- throw new Error("VITE_BASE_PATH must be defined");
116
- }
117
- const basePath = configuredBasePath.replace(/\\/$/, "");
118
- const normalizedPath = path.startsWith("/") ? path : \`/\${path}\`;
119
- if (basePath === "" || basePath === "/") {
120
- return normalizedPath;
121
- }
122
- return \`\${basePath}\${normalizedPath}\`;
123
- }
124
- `;
125
-
126
- // Helper functions
127
- function ensureDir(dir) {
128
- if (!fs.existsSync(dir)) {
129
- fs.mkdirSync(dir, { recursive: true });
130
- }
131
- }
132
-
133
- function writeFile(filePath, content, overwrite = false) {
134
- const relativePath = path.relative(cwd, filePath);
135
-
136
- if (fs.existsSync(filePath) && !overwrite) {
137
- // Check if content is different
138
- const existing = fs.readFileSync(filePath, "utf-8");
139
- if (existing === content) {
140
- console.log(dim(` ${relativePath} (unchanged)`));
141
- return false;
142
- }
143
- console.log(yellow(` ${relativePath} (exists, skipping - use --force to overwrite)`));
144
- return false;
145
- }
146
-
147
- fs.writeFileSync(filePath, content);
148
- console.log(green(` ${relativePath}`));
149
- return true;
150
- }
151
-
152
- function checkViteConfig() {
153
- const viteConfigPath = path.join(cwd, "vite.config.ts");
154
- if (!fs.existsSync(viteConfigPath)) {
155
- console.log(yellow(" vite.config.ts not found - skipping validation"));
156
- return;
157
- }
158
-
159
- const content = fs.readFileSync(viteConfigPath, "utf-8");
160
-
161
- if (!content.includes("getSharedViteConfig")) {
162
- console.log();
163
- console.log(yellow("Warning: vite.config.ts does not use getSharedViteConfig()"));
164
- console.log(dim("Consider updating it to use the shared config:"));
165
- console.log();
166
- console.log(dim(` import { getSharedViteConfig } from "@platform/app-shell/vite";`));
167
- console.log(dim(` import { mergeConfig } from "vite";`));
168
- console.log();
169
- console.log(dim(` export default defineConfig(({ mode }) => {`));
170
- console.log(dim(` const shared = getSharedViteConfig(__dirname);`));
171
- console.log(dim(` return mergeConfig(shared, { /* app-specific config */ });`));
172
- console.log(dim(` });`));
173
- } else {
174
- console.log(green(" vite.config.ts uses getSharedViteConfig()"));
175
- }
176
- }
177
-
178
- function checkConflictingCss() {
179
- // Check for app.css or other CSS files that might conflict with app-shell
180
- const conflictingFiles = ["app/app.css", "app/global.css", "app/globals.css"];
181
- const found = [];
182
-
183
- for (const file of conflictingFiles) {
184
- const filePath = path.join(cwd, file);
185
- if (fs.existsSync(filePath)) {
186
- const content = fs.readFileSync(filePath, "utf-8");
187
- // Check if it has html/body/root blocks with hardcoded colors
188
- // These patterns specifically target global style overrides
189
- const hasGlobalColorOverride =
190
- /(?:html|body|\:root)\s*\{[^}]*(?:background-color|background|color)\s*:/i.test(content);
191
-
192
- if (hasGlobalColorOverride) {
193
- found.push(file);
194
- }
195
- }
196
- }
197
-
198
- if (found.length > 0) {
199
- console.log();
200
- console.log(yellow("Warning: Found CSS files that may conflict with app-shell:"));
201
- for (const file of found) {
202
- console.log(red(` ${file}`));
203
- }
204
- console.log(dim("These files have html/body/:root blocks with hardcoded colors."));
205
- console.log(dim("Remove these blocks and rely on app-shell/styles.css for theming."));
206
- } else {
207
- console.log(green(" No conflicting CSS files found"));
208
- }
209
- }
210
-
211
- // Check for --force flag
212
- const force = process.argv.includes("--force") || process.argv.includes("-f");
213
-
214
- // Preflight checks
215
- console.log("Preflight checks:");
216
-
217
- // Check if we're in a frontend directory
218
- const packageJsonPath = path.join(cwd, "package.json");
219
- if (!fs.existsSync(packageJsonPath)) {
220
- console.log(red(" package.json not found"));
221
- console.log(dim(" Run this command from your frontend directory"));
222
- process.exit(1);
223
- }
224
- console.log(green(" Found package.json"));
225
-
226
- // Check for app directory (React Router convention)
227
- const appDir = path.join(cwd, "app");
228
- if (!fs.existsSync(appDir)) {
229
- console.log(yellow(" app/ directory not found - will create it"));
230
- }
231
- console.log(green(" Found app/ directory"));
232
-
233
- // Check for @platform/app-shell dependency
234
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
235
- const hasAppShell = packageJson.dependencies?.["@platform/app-shell"] ||
236
- packageJson.devDependencies?.["@platform/app-shell"];
237
- if (!hasAppShell) {
238
- console.log(yellow(" @platform/app-shell not in dependencies"));
239
- console.log(dim(" Add it: \"@platform/app-shell\": \"file:../../../shared/packages/app-shell\""));
240
- } else {
241
- console.log(green(" Found @platform/app-shell dependency"));
242
- }
243
-
244
- console.log();
245
- console.log("Creating files:");
246
-
247
- // Ensure directories exist
248
- ensureDir(path.join(cwd, "app"));
249
- ensureDir(path.join(cwd, "app/lib"));
250
-
251
- // Create files
252
- writeFile(path.join(cwd, "app/tailwind.css"), TAILWIND_CSS, force);
253
- writeFile(path.join(cwd, "tailwind.config.ts"), TAILWIND_CONFIG, force);
254
- writeFile(path.join(cwd, "components.json"), COMPONENTS_JSON, force);
255
- writeFile(path.join(cwd, "app/lib/utils.ts"), UTILS_TS, force);
256
-
257
- console.log();
258
- console.log("Validating configuration:");
259
- checkViteConfig();
260
- checkConflictingCss();
261
-
262
- console.log();
263
- console.log(green("Done!"));
264
- console.log();
265
- console.log("Next steps:");
266
- console.log(dim(" 1. Run 'bun install' to install dependencies"));
267
- console.log(dim(" 2. Import '@platform/app-shell' in your layout component"));
268
- console.log(dim(" 3. Run 'bun run dev' to start the dev server"));
269
- console.log();