Sample application for blog post

This commit is contained in:
Alex Villarreal 2024-05-17 18:58:54 -05:00
Родитель 334f4a96af
Коммит 989c2efc9f
20 изменённых файлов: 11038 добавлений и 0 удалений

Просмотреть файл

@ -0,0 +1,5 @@
# .env.defaults should not contain private information. Create a .env file locally to override these values.
AZURE_TENANT_ID=<tenant-id>
AZURE_ORDERER=<orderer-url>
AZURE_FUNCTION_TOKEN_PROVIDER_URL=<token-provider-url>
FLUID_CLIENT=azure

36
dice-roller/.eslintrc.cjs Normal file
Просмотреть файл

@ -0,0 +1,36 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
module.exports = {
"env": {
"browser": true,
"es2021": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": [
"react",
"@typescript-eslint"
],
"settings": {
"react": {
"version": "detect",
},
},
"rules": {
"no-undef": "off",
}
}

5
dice-roller/.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,5 @@
shell.nix
**/node_modules/
dist/
.env

Просмотреть файл

@ -0,0 +1,8 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
module.exports = {
...require("@fluidframework/build-common/prettier.config.cjs"),
};

21
dice-roller/LICENSE Normal file
Просмотреть файл

@ -0,0 +1,21 @@
Copyright (c) Microsoft Corporation and contributors. All rights reserved.
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

73
dice-roller/README.md Normal file
Просмотреть файл

@ -0,0 +1,73 @@
# Shared Tree Dice Roller Demo
This app is a simple dice roller that uses the Fluid Framework SharedTree data structure to manage the dice rolls.
The app is built using React and the Fluid Framework.
This is a companion app for a blog post on using Fluid Devtools.
## Setting up the Fluid Framework
This app is designed to use [Azure Fluid Relay](https://aka.ms/azurefluidrelay) a Fluid relay service offered by Microsoft.
You can also run a local service for development purposes.
Instructions on how to set up a Fluid relay are on the [Fluid Framework website](https://aka.ms/fluid).
To use AzureClient's local mode, you first need to start a local server.
```bash
npm run start:server
```
Running this command from your terminal window will launch the Azure Fluid Relay local server.
Once the server is started, you can run your application against the local service.
```bash
npm run start
```
One important note is that you will need to use a token provider or, purely for testing and development, use the insecure token provider.
There are instructions on how to set this up on the [Fluid Framework website](https://aka.ms/fluid).
All the code required to set up the Fluid Framework and SharedTree data structure is in the infra folder.
Most of this code will be the same for any app.
## Schema Definition
The SharedTree schema is defined in the schema.ts source file.
This schema is passed into the SharedTree when it is initialized in index.tsx.
For more details, see the schema.ts comments.
## Working with Data
Working with data in the SharedTree is very simple; however, working with distributed data is always a little more
complicated than working with local data.
One important note about managing local state and events: ideally, in any app you write, it is best to not special
case local changes.
Treat the SharedTree as your local data and rely on tree events to update your view.
This makes the code reliable and easy to maintain.
Also, never mutate tree nodes within events listeners.
## User Interface
This app is built using React.
If you want to change the css you must run 'npx tailwindcss -i ./src/index.css -o ./src/output.css --watch' in the
root folder of your project so that tailwind can update the output.css file.
## Building and Running
You can use the following npm scripts (`npm run SCRIPT-NAME`) to build and run the app.
<!-- AUTO-GENERATED-CONTENT:START (SCRIPTS) -->
| Script | Description |
| ----------- | ------------------------------------------------------------------------------------- |
| `build` | `npm run format && npm run webpack` |
| `compile` | Compile the TypeScript source code to JavaScript. |
| `dev` | Runs the app in webpack-dev-server. Expects local-azure-service running on port 7070. |
| `dev:azure` | Runs the app in webpack-dev-server using the Azure Fluid Relay config. |
| `docs` | Update documentation. |
| `format` | Format source code using Prettier. |
| `lint` | Lint source code using ESLint |
| `webpack` | `webpack` |
| `start` | `npm run dev` |
<!-- AUTO-GENERATED-CONTENT:END -->

Просмотреть файл

@ -0,0 +1,15 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
module.exports = {
matchWord: 'AUTO-GENERATED-CONTENT',
transforms: {
/* Match <!-- AUTO-GENERATED-CONTENT:START (SCRIPTS) --> */
SCRIPTS: require('markdown-magic-package-scripts'),
},
callback: function () {
console.log('markdown processing done')
}
}

9479
dice-roller/package-lock.json сгенерированный Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

71
dice-roller/package.json Normal file
Просмотреть файл

@ -0,0 +1,71 @@
{
"name": "item-counter",
"version": "0.0.1",
"private": "true",
"description": "",
"license": "MIT",
"type": "module",
"scripts": {
"build": "npm run format && npm run webpack",
"ci:test": "start-server-and-test start:server 7070 ci:test:jest",
"ci:test:jest": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --ci --reporters=default --reporters=jest-junit",
"compile": "tsc -b",
"dev": "cross-env FLUID_CLIENT='local' webpack-dev-server",
"dev:azure": "cross-env FLUID_CLIENT='azure' webpack-dev-server",
"format": "prettier src --write",
"lint": "eslint src",
"webpack": "cross-env FLUID_CLIENT='azure' webpack",
"start": "npm run dev",
"start:server": "npx @fluidframework/azure-local-service@2.0.0-rc.4.0.1"
},
"engines": {
"node": ">=20.0.0"
},
"dependencies": {
"@fluentui/react-icons": "^2.0.221",
"@fluidframework/azure-client": "2.0.0-rc.4.0.1",
"@fluidframework/telemetry-utils": "2.0.0-rc.4.0.1",
"@fluidframework/test-runtime-utils": "2.0.0-rc.4.0.1",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"axios": "^1.6.2",
"dotenv": "^16.0.2",
"fluid-framework": "2.0.0-rc.4.0.1",
"guid-typescript": "^1.0.9",
"hashids": "^2.2.10",
"jsrsasign": "^11.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"uuid": "^9.0.1"
},
"devDependencies": {
"@fluidframework/build-common": "^2.0.0",
"@fluidframework/devtools": "2.0.0-rc.4.0.1",
"@types/debug": "^4.1.7",
"@types/jsrsasign": "^10.5.13",
"@types/uuid": "^9.0.8",
"@typescript-eslint/eslint-plugin": "^5.38.0",
"@typescript-eslint/parser": "^5.38.0",
"copy-webpack-plugin": "^11.0.0",
"cross-env": "^7.0.3",
"css-loader": "^5",
"dotenv-webpack": "^7.1.1",
"eslint": "^8.23.1",
"eslint-plugin-react": "^7.31.8",
"html-webpack-plugin": "^4.5.2",
"mini-css-extract-plugin": "^1",
"prettier": "^2.7.1",
"start-server-and-test": "^2.0.0",
"style-loader": "^2",
"tailwindcss": "^3.3.2",
"ts-loader": "^8.0.0",
"typescript": "~5.4",
"webpack": "^5.88.2",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1"
},
"jest-junit": {
"outputDirectory": "nyc",
"outputName": "jest-junit-report.xml"
}
}

Просмотреть файл

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

71
dice-roller/src/index.tsx Normal file
Просмотреть файл

@ -0,0 +1,71 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
/* eslint-disable react/jsx-key */
import React from "react";
import { createRoot } from "react-dom/client";
import { loadFluidData, containerSchema } from "./infra/fluid.js";
import { treeConfiguration } from "./schema.js";
import "./output.css";
import { ReactApp } from "./react_app.js";
import { AttachState, Tree } from "fluid-framework";
import { initializeDevtools } from "@fluidframework/devtools/beta";
import { devtoolsLogger } from "./infra/clientProps.js";
async function start() {
// create the root element for React
const app = document.createElement("div");
app.id = "app";
document.body.appendChild(app);
const root = createRoot(app);
// Get the root container id from the URL
// If there is no container id, then the app will make
// a new container.
let containerId = location.hash.substring(1);
// Initialize Fluid Container - this will either make a new container or load an existing one
const { container } = await loadFluidData(containerId, containerSchema);
// Initialize the SharedTree Data Structure
const appData = container.initialObjects.appData.schematize(
treeConfiguration, // This is defined in schema.ts
);
// Render the app - note we attach new containers after render so
// the app renders instantly on create new flow. The app will be
// interactive immediately.
root.render(<ReactApp data={appData} />);
initializeDevtools({
logger: devtoolsLogger,
initialContainers: [
{
container,
containerKey: "My Container",
},
],
});
// If the app is in a `createNew` state - no containerId, and the container is detached, we attach the container.
// This uploads the container to the service and connects to the collaboration session.
if (container.attachState === AttachState.Detached) {
containerId = await container.attach();
// The newly attached container is given a unique ID that can be used to access the container in another session.
// This adds that id to the url.
history.replaceState(undefined, "", "#" + containerId);
} else {
// When loading an existing container...
Tree.on(appData.root, "treeChanged", () => {
for (let i = 0; i < 1_000_000_000; i++) {
/* Simulate long-running application logic */
}
});
}
}
start().catch((error) => console.error(error));

Просмотреть файл

@ -0,0 +1,49 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
import {
AzureRemoteConnectionConfig,
AzureClientProps,
AzureLocalConnectionConfig,
} from "@fluidframework/azure-client";
import {
AzureFunctionTokenProvider,
azureUser,
InsecureTokenProvider,
user,
} from "./tokenProvider.js";
import { createDevtoolsLogger } from "@fluidframework/devtools/beta";
const useAzure = process.env.FLUID_CLIENT === "azure";
if (!useAzure) {
console.warn(`Configured to use local tinylicious.`);
}
const remoteConnectionConfig: AzureRemoteConnectionConfig = {
type: "remote",
tenantId: process.env.AZURE_TENANT_ID!,
tokenProvider: new AzureFunctionTokenProvider(
process.env.AZURE_FUNCTION_TOKEN_PROVIDER_URL!,
azureUser,
),
endpoint: process.env.AZURE_ORDERER!,
};
const localConnectionConfig: AzureLocalConnectionConfig = {
type: "local",
tokenProvider: new InsecureTokenProvider("VALUE_NOT_USED", user),
endpoint: "http://localhost:7070",
};
export const devtoolsLogger = createDevtoolsLogger();
const connectionConfig: AzureRemoteConnectionConfig | AzureLocalConnectionConfig = useAzure
? remoteConnectionConfig
: localConnectionConfig;
export const clientProps: AzureClientProps = {
connection: connectionConfig,
logger: devtoolsLogger,
};

Просмотреть файл

@ -0,0 +1,45 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
import { AzureClient, AzureContainerServices } from "@fluidframework/azure-client";
import { ContainerSchema, IFluidContainer, SharedTree } from "fluid-framework";
import { clientProps } from "./clientProps.js";
const client = new AzureClient(clientProps);
/**
* This function will create a container if no container ID is passed on the hash portion of the URL.
* If a container ID is provided, it will load the container.
*
* @returns The loaded container and container services.
*/
export async function loadFluidData<T extends ContainerSchema>(
containerId: string,
containerSchema: T,
): Promise<{
services: AzureContainerServices;
container: IFluidContainer<T>;
}> {
let container: IFluidContainer<T>;
let services: AzureContainerServices;
// Get or create the document depending if we are running through the create new flow
if (containerId.length === 0) {
// The client will create a new detached container using the schema
// A detached container will enable the app to modify the container before attaching it to the client
({ container, services } = await client.createContainer(containerSchema));
} else {
// Use the unique container ID to fetch the container created earlier. It will already be connected to the
// collaboration session.
({ container, services } = await client.getContainer(containerId, containerSchema));
}
return { services, container };
}
export const containerSchema = {
initialObjects: {
appData: SharedTree,
},
} satisfies ContainerSchema;

Просмотреть файл

@ -0,0 +1,202 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
import {
AzureMember,
ITokenClaims,
ITokenProvider,
ITokenResponse,
IUser,
} from "@fluidframework/azure-client";
import { ScopeType } from "@fluidframework/protocol-definitions";
import axios from "axios";
import { KJUR as jsrsasign } from "jsrsasign";
import { v4 as uuid } from "uuid";
/**
* Insecure user definition. *
*/
export interface IInsecureUser extends IUser {
/**
* Name of the user making the connection to the service.
*/
name: string;
}
/**
* Token Provider implementation for connecting to an Azure Function endpoint for
* Azure Fluid Relay token resolution.
*/
export class AzureFunctionTokenProvider implements ITokenProvider {
/**
* Creates a new instance using configuration parameters.
* @param azFunctionUrl - URL to Azure Function endpoint
* @param user - User object
*/
constructor(
private readonly azFunctionUrl: string,
private readonly user?: Pick<AzureMember, "name" | "id" | "additionalDetails">,
) {}
public async fetchOrdererToken(tenantId: string, documentId?: string): Promise<ITokenResponse> {
return {
jwt: await this.getToken(tenantId, documentId),
};
}
public async fetchStorageToken(tenantId: string, documentId: string): Promise<ITokenResponse> {
return {
jwt: await this.getToken(tenantId, documentId),
};
}
private async getToken(tenantId: string, documentId: string | undefined): Promise<string> {
const response = await axios.get(this.azFunctionUrl, {
params: {
tenantId,
documentId,
userName: this.user?.name,
userId: this.user?.id,
additionalDetails: this.user?.additionalDetails,
},
});
return response.data as string;
}
}
/**
* Provides an in memory implementation of a Fluid Token Provider that can be
* used to insecurely connect to the Fluid Relay.
*
* As the name implies, this is not secure and should not be used in production.
* It simply makes examples where authentication is not relevant easier to bootstrap.
*/
export class InsecureTokenProvider implements ITokenProvider {
constructor(
/**
* Private server tenantKey for generating tokens.
*/
private readonly tenantKey: string,
/**
* User with whom generated tokens will be associated.
*/
private readonly user: IInsecureUser,
) {}
/**
* {@inheritDoc @fluidframework/routerlicious-driver#ITokenProvider.fetchOrdererToken}
*/
public async fetchOrdererToken(tenantId: string, documentId?: string): Promise<ITokenResponse> {
return {
fromCache: true,
jwt: generateToken(
tenantId,
this.tenantKey,
[ScopeType.DocRead, ScopeType.DocWrite, ScopeType.SummaryWrite],
documentId,
this.user,
),
};
}
/**
* {@inheritDoc @fluidframework/routerlicious-driver#ITokenProvider.fetchStorageToken}
*/
public async fetchStorageToken(tenantId: string, documentId: string): Promise<ITokenResponse> {
return {
fromCache: true,
jwt: generateToken(
tenantId,
this.tenantKey,
[ScopeType.DocRead, ScopeType.DocWrite, ScopeType.SummaryWrite],
documentId,
this.user,
),
};
}
}
export const user = generateUser();
export const azureUser = {
id: user.id,
name: user.name,
};
/**
* Generates a {@link https://en.wikipedia.org/wiki/JSON_Web_Token | JSON Web Token} (JWT)
* to authorize access to a Routerlicious-based Fluid service.
*
* @remarks Note: this function uses a browser friendly auth library
* ({@link https://www.npmjs.com/package/jsrsasign | jsrsasign}) and may only be used in client (browser) context.
* It is **not** Node.js-compatible.
*
* @param tenantId - See {@link @fluidframework/protocol-definitions#ITokenClaims.tenantId}
* @param key - API key to authenticate user. Must be {@link https://en.wikipedia.org/wiki/UTF-8 | UTF-8}-encoded.
* @param scopes - See {@link @fluidframework/protocol-definitions#ITokenClaims.scopes}
* @param documentId - See {@link @fluidframework/protocol-definitions#ITokenClaims.documentId}.
* If not specified, the token will not be associated with a document, and an empty string will be used.
* @param user - User with whom generated tokens will be associated.
* If not specified, the token will not be associated with a user, and a randomly generated mock user will be
* used instead.
* See {@link @fluidframework/protocol-definitions#ITokenClaims.user}
* @param lifetime - Used to generate the {@link @fluidframework/protocol-definitions#ITokenClaims.exp | expiration}.
* Expiration = now + lifetime.
* Expressed in seconds.
* Default: 3600 (1 hour).
* @param ver - See {@link @fluidframework/protocol-definitions#ITokenClaims.ver}.
* Default: `1.0`.
*/
export function generateToken(
tenantId: string,
key: string,
scopes: ScopeType[],
documentId?: string,
user?: IInsecureUser,
lifetime: number = 60 * 60,
ver = "1.0",
): string {
let userClaim = user ? user : generateUser();
if (userClaim.id === "" || userClaim.id === undefined) {
userClaim = generateUser();
}
// Current time in seconds
const now = Math.round(Date.now() / 1000);
const docId = documentId ?? "";
const claims: ITokenClaims & { jti: string } = {
documentId: docId,
scopes,
tenantId,
user: userClaim,
iat: now,
exp: now + lifetime,
ver,
jti: uuid(),
};
const utf8Key = { utf8: key };
return jsrsasign.jws.JWS.sign(
null,
JSON.stringify({ alg: "HS256", typ: "JWT" }),
claims,
utf8Key,
);
}
/**
* Generates an arbitrary ("random") {@link IInsecureUser} by generating a
* random UUID for its {@link @fluidframework/protocol-definitions#IUser.id | id} and {@link IInsecureUser.name | name} properties.
*/
export function generateUser(): IInsecureUser {
const randomUser = {
id: uuid(),
name: uuid(),
};
return randomUser;
}

758
dice-roller/src/output.css Normal file
Просмотреть файл

@ -0,0 +1,758 @@
/*
! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com
*/
/*
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
*/
*,
::before,
::after {
box-sizing: border-box;
/* 1 */
border-width: 0;
/* 2 */
border-style: solid;
/* 2 */
border-color: #e5e7eb;
/* 2 */
}
::before,
::after {
--tw-content: "";
}
/*
1. Use a consistent sensible line-height in all browsers.
2. Prevent adjustments of font size after orientation changes in iOS.
3. Use a more readable tab size.
4. Use the user's configured `sans` font-family by default.
5. Use the user's configured `sans` font-feature-settings by default.
6. Use the user's configured `sans` font-variation-settings by default.
7. Disable tap highlights on iOS
*/
html,
:host {
line-height: 1.5;
/* 1 */
-webkit-text-size-adjust: 100%;
/* 2 */
-moz-tab-size: 4;
/* 3 */
-o-tab-size: 4;
tab-size: 4;
/* 3 */
font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
"Segoe UI Symbol", "Noto Color Emoji";
/* 4 */
font-feature-settings: normal;
/* 5 */
font-variation-settings: normal;
/* 6 */
-webkit-tap-highlight-color: transparent;
/* 7 */
}
/*
1. Remove the margin in all browsers.
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
*/
body {
margin: 0;
/* 1 */
line-height: inherit;
/* 2 */
}
/*
1. Add the correct height in Firefox.
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
3. Ensure horizontal rules are visible by default.
*/
hr {
height: 0;
/* 1 */
color: inherit;
/* 2 */
border-top-width: 1px;
/* 3 */
}
/*
Add the correct text decoration in Chrome, Edge, and Safari.
*/
abbr:where([title]) {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
/*
Remove the default font size and weight for headings.
*/
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit;
}
/*
Reset links to optimize for opt-in styling instead of opt-out.
*/
a {
color: inherit;
text-decoration: inherit;
}
/*
Add the correct font weight in Edge and Safari.
*/
b,
strong {
font-weight: bolder;
}
/*
1. Use the user's configured `mono` font-family by default.
2. Use the user's configured `mono` font-feature-settings by default.
3. Use the user's configured `mono` font-variation-settings by default.
4. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp,
pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace;
/* 1 */
font-feature-settings: normal;
/* 2 */
font-variation-settings: normal;
/* 3 */
font-size: 1em;
/* 4 */
}
/*
Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/*
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/*
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
3. Remove gaps between table borders by default.
*/
table {
text-indent: 0;
/* 1 */
border-color: inherit;
/* 2 */
border-collapse: collapse;
/* 3 */
}
/*
1. Change the font styles in all browsers.
2. Remove the margin in Firefox and Safari.
3. Remove default padding in all browsers.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
/* 1 */
font-feature-settings: inherit;
/* 1 */
font-variation-settings: inherit;
/* 1 */
font-size: 100%;
/* 1 */
font-weight: inherit;
/* 1 */
line-height: inherit;
/* 1 */
letter-spacing: inherit;
/* 1 */
color: inherit;
/* 1 */
margin: 0;
/* 2 */
padding: 0;
/* 3 */
}
/*
Remove the inheritance of text transform in Edge and Firefox.
*/
button,
select {
text-transform: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Remove default button styles.
*/
button,
input:where([type="button"]),
input:where([type="reset"]),
input:where([type="submit"]) {
-webkit-appearance: button;
/* 1 */
background-color: transparent;
/* 2 */
background-image: none;
/* 2 */
}
/*
Use the modern Firefox focus style for all focusable elements.
*/
:-moz-focusring {
outline: auto;
}
/*
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
*/
:-moz-ui-invalid {
box-shadow: none;
}
/*
Add the correct vertical alignment in Chrome and Firefox.
*/
progress {
vertical-align: baseline;
}
/*
Correct the cursor style of increment and decrement buttons in Safari.
*/
::-webkit-inner-spin-button,
::-webkit-outer-spin-button {
height: auto;
}
/*
1. Correct the odd appearance in Chrome and Safari.
2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
/* 2 */
}
/*
Remove the inner padding in Chrome and Safari on macOS.
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button;
/* 1 */
font: inherit;
/* 2 */
}
/*
Add the correct display in Chrome and Safari.
*/
summary {
display: list-item;
}
/*
Removes the default spacing and border for appropriate elements.
*/
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;
}
/*
Reset default styling for dialogs.
*/
dialog {
padding: 0;
}
/*
Prevent resizing textareas horizontally by default.
*/
textarea {
resize: vertical;
}
/*
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
2. Set the default placeholder color to the user's configured gray 400 color.
*/
input::-moz-placeholder,
textarea::-moz-placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
input::placeholder,
textarea::placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
/*
Set the default cursor for buttons.
*/
button,
[role="button"] {
cursor: pointer;
}
/*
Make sure disabled buttons don't get the pointer cursor.
*/
:disabled {
cursor: default;
}
/*
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
This can trigger a poorly considered lint error in some tools but is included by design.
*/
img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
display: block;
/* 1 */
vertical-align: middle;
/* 2 */
}
/*
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
*/
img,
video {
max-width: 100%;
height: auto;
}
/* Make elements with the HTML hidden attribute stay hidden by default */
[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 / 0.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: ;
--tw-contain-size: ;
--tw-contain-layout: ;
--tw-contain-paint: ;
--tw-contain-style: ;
}
::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 / 0.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: ;
--tw-contain-size: ;
--tw-contain-layout: ;
--tw-contain-paint: ;
--tw-contain-style: ;
}
.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;
}
}
.m-6 {
margin: 1.5rem;
}
.my-8 {
margin-top: 2rem;
margin-bottom: 2rem;
}
.flex {
display: flex;
}
.h-24 {
height: 6rem;
}
.h-full {
height: 100%;
}
.w-24 {
width: 6rem;
}
.w-full {
width: 100%;
}
.max-w-sm {
max-width: 24rem;
}
.flex-row {
flex-direction: row;
}
.flex-col {
flex-direction: column;
}
.flex-wrap {
flex-wrap: wrap;
}
.content-center {
align-content: center;
}
.items-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
.gap-3 {
gap: 0.75rem;
}
.gap-4 {
gap: 1rem;
}
.rounded {
border-radius: 0.25rem;
}
.rounded-full {
border-radius: 9999px;
}
.bg-black {
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
}
.bg-blue-500 {
--tw-bg-opacity: 1;
background-color: rgb(59 130 246 / var(--tw-bg-opacity));
}
.bg-transparent {
background-color: transparent;
}
.p-4 {
padding: 1rem;
}
.px-4 {
padding-left: 1rem;
padding-right: 1rem;
}
.py-2 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.pt-2 {
padding-top: 0.5rem;
}
.text-center {
text-align: center;
}
.text-4xl {
font-size: 2.25rem;
line-height: 2.5rem;
}
.text-base {
font-size: 1rem;
line-height: 1.5rem;
}
.text-xl {
font-size: 1.25rem;
line-height: 1.75rem;
}
.font-bold {
font-weight: 700;
}
.font-extrabold {
font-weight: 800;
}
.text-blue-300 {
--tw-text-opacity: 1;
color: rgb(147 197 253 / var(--tw-text-opacity));
}
.text-white {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.shadow-md {
--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color),
0 2px 4px -2px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.hover\:bg-blue-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(29 78 216 / var(--tw-bg-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;
}

Просмотреть файл

@ -0,0 +1,58 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
import React, { ReactNode, useEffect, useState } from "react";
import { TreeView, Tree } from "fluid-framework";
import { DiceRoller } from "./schema.js";
export function ReactApp(props: { data: TreeView<typeof DiceRoller> }): JSX.Element {
const [invalidations, setInvalidations] = useState(0);
const diceRoller = props.data.root;
// Register for tree deltas when the component mounts.
// Any time the tree changes, the app will update.
useEffect(() => {
const unsubscribe = Tree.on(diceRoller, "treeChanged", () => {
setInvalidations(invalidations + Math.random());
});
return unsubscribe;
}, []);
return (
<div className="flex flex-row gap-3 justify-center flex-wrap w-full h-full">
<div className="flex flex-col gap-3 justify-center content-center m-4">
<VisualDie diceRoller={diceRoller} />
<RollButton diceRoller={diceRoller} />
</div>
</div>
);
}
export function VisualDie(props: { diceRoller: DiceRoller }): JSX.Element {
const divStyle = {
// color: `hsl(${parseInt(props.diceRoller.value, 10) * 60}, 70%, 50%)`,
color: `hsl(${props.diceRoller.value * 60}, 70%, 50%)`,
fontSize: "200px",
};
// Unicode 0x2680-0x2685 are the sides of a dice (⚀⚁⚂⚃⚄⚅)
return (
<div style={divStyle}>
{String.fromCodePoint((0x267f + props.diceRoller.value) as unknown as number)}
</div>
);
}
export function RollButton(props: { diceRoller: DiceRoller }): JSX.Element {
return (
<button
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded w-full"
onClick={() => props.diceRoller.roll()}
>
Roll
</button>
);
}

33
dice-roller/src/schema.ts Normal file
Просмотреть файл

@ -0,0 +1,33 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
import { TreeConfiguration, SchemaFactory } from "fluid-framework";
// Define a schema factory that is used to generate classes for the schema
const schemaFactory = new SchemaFactory("d302b84c-75f6-4ecd-9663-524f467013e3");
export class DiceRoller extends schemaFactory.object("DiceRoller", {
// value: schemaFactory.string
value: schemaFactory.number
}) {
public roll() {
let newValue = -1;
// while (newValue === parseInt(this.value, 10) || newValue === -1) {
while (newValue === this.value || newValue === -1) {
newValue = Math.floor(Math.random() * 6) + 1;
}
// this.value = newValue.toString();
this.value = newValue;
}
}
// This object is passed into the SharedTree via the schematize method.
export const treeConfiguration = new TreeConfiguration(
// Specify the root type - our class.
DiceRoller,
// Initial state of the tree which is used for new trees.
// () => new DiceRoller({ value: "1" }),
() => new DiceRoller({ value: 1 }),
);

Просмотреть файл

@ -0,0 +1,13 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{js,jsx,ts,tsx}'],
theme: {
extend: {},
},
plugins: [],
};

15
dice-roller/tsconfig.json Normal file
Просмотреть файл

@ -0,0 +1,15 @@
{
"compilerOptions": {
// DO NOT EDIT
"outDir": "./dist",
"target": "es6",
"module": "Node16",
"sourceMap": true,
// Set your preferences here for TypeScript
"strict": true,
"jsx": "react",
"moduleResolution": "Node16",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
},
}

Просмотреть файл

@ -0,0 +1,78 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
/* eslint-disable @typescript-eslint/no-var-requires */
const path = require("path");
const Dotenv = require("dotenv-webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
// Basic configuration
entry: "./src/index.tsx",
// Necessary in order to use source maps and debug directly TypeScript files
devtool: "source-map",
mode: "development",
performance: {
maxAssetSize: 4000000,
maxEntrypointSize: 4000000,
},
module: {
rules: [
// Necessary in order to use TypeScript
{
test: /\.ts$|tsx/,
use: "ts-loader",
exclude: /node_modules/,
},
{
test: /\.css$/,
use: [
{
loader: "style-loader",
},
{
loader: "css-loader",
},
],
},
],
},
resolve: {
extensionAlias: {
".js": [".ts", ".tsx", ".js", ".cjs", ".mjs"],
},
extensions: [".ts", ".tsx", ".js", ".cjs", ".mjs"],
},
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist"),
// This line is VERY important for VS Code debugging to attach properly
// Tamper with it at your own risks
devtoolModuleFilenameTemplate: "[absolute-resource-path]",
clean: true,
},
plugins: [
// No need to write a index.html
new HtmlWebpackPlugin({
title: "Fluid Demo",
favicon: "",
}),
// Load environment variables during webpack bundle
new Dotenv({
systemvars: true,
}),
// Extract CSS to separate file
new MiniCssExtractPlugin({
filename: "css/mystyles.css",
}),
],
devServer: {
// keep port in sync with VS Code launch.json
port: 8080,
// Hot-reloading, the sole reason to use webpack here <3
hot: true,
},
};